# Welcome to the Homey Apps SDK 👋

Learn how to build great apps that run on Homey.

**Homey is a smart home platform that connects devices from various brands & technologies in one unified experience. Homey owners can control their devices in the Homey mobile & web app, create automations called Flow, learn their energy usage with Homey Energy and view charts with Homey Insights.**

The Homey Apps SDK enables developers to create apps that run on Homey. An app for Homey is a [Node.js](https://en.wikipedia.org/wiki/Node.js) or [Python](https://en.wikipedia.org/wiki/Python_\(programming_language\)) bundle distributed through the [Homey App Store](https://homey.app/apps), or installed by [Homey CLI](https://www.npmjs.com/package/homey). These apps run local on Homey, similar to an iPhone or Android device.

With the Homey Apps SDK your app can extend Homey by adding new [Devices](/the-basics/devices) and create new [Flow cards](/the-basics/flow), among other things. Additionally, your app can transmit and receive wireless signals, such as [Wi-Fi](/wireless/wi-fi), [Zigbee](/wireless/zigbee), [Z-Wave](/wireless/z-wave), [433 MHz](/wireless/rf-433mhz-868mhz), [Bluetooth LE ](/wireless/bluetooth)and [Infrared](/wireless/infrared).

## 🚀 Getting Started

To get started with your first Homey app, head over to [Getting Started »](/the-basics/getting-started)

## 👥 Community

* Questions? Ask them on [Stack Overflow](https://stackoverflow.com/questions/ask?tags=homey) with the `homey` tag.
* Please report any issues you find in the [Apps SDK Issue Tracker](https://github.com/athombv/homey-apps-sdk-issues/issues).

![](/files/j09cC2cFgyxTJp6EzgC4)


# Getting Started

How to create your first Homey app.

This guide is dedicated to getting you up and running with Homey App development within about 15 minutes. Even if you have no previous experience with the tools being used, this guide should have you covered.

{% embed url="<https://www.youtube.com/watch?v=v_RUamZzby8>" %}

## 1. Install Homey CLI

The [Homey command-line interface (CLI)](/the-basics/getting-started/homey-cli) is the tool you need for Homey App development. It enables you to create and run apps on Homey for development and debugging purposes. Additionally, it contains various utilities that make Homey App development faster and easier.

To use the Homey CLI you will need to have the following prerequisites installed:

* Node.js v24 or higher
* Docker, for Homey Cloud, Homey Pro, and Homey Self-Hosted Server

You can download the official Node.js installer from [the Node.js website](https://nodejs.org/). We recommend installing a Node.js version using the [Node Version Manager (NVM)](https://github.com/nvm-sh/nvm). This tool allows you to easily install and switch between different Node.js versions.

If you want to create apps written in Python, or that run in our Cloud environment, you will also need to have Docker running on your machine to test your apps. You can download the Docker installer from [the Docker website](https://docs.docker.com/desktop/).

By installing Node.js you have gained access to the [Node Package Manager (NPM)](https://www.npmjs.com/). This is a command-line tool that manages dependencies of Node.js projects. NPM allows you to install Homey CLI with a single command.

Open a command line and execute the following command. This installs the Homey CLI globally on your system and exposes the `homey` command.

```bash
npm install --global homey
```

{% hint style="info" %}
You may need superuser rights to install packages globally.
{% endhint %}

## 2. Create a Homey app

After installing the Homey CLI you can use the `homey app create` command to create a basic project directory for your Homey App. This command will start an interactive prompt that asks a few questions such as what the ID of your app should be.

```bash
homey app create
```

The ID of your app should be in "[reverse domain name notation](https://en.wikipedia.org/wiki/Reverse_domain_name_notation)" so if you're creating a Homey App for `https://solarpanels.acme.org`, the ID should be `org.acme.solarpanels`.

{% hint style="info" %}
Note: the Homey or Athom name cannot be used in your app ID.
{% endhint %}

After answering the questions the CLI will create a new Homey app in a directory named after your chosen Homey App ID. The rest of this guide expects that your current working directory is the Homey App folder so you should change your directory to it:

```bash
cd org.acme.solarpanels
```

The project directory will contain a number of files and folders required for your Homey App to run on Homey. Don't be scared by this, you don't need to understand what all these files and folders mean at this point, we will explain the purpose of all the files and folders in the next section.

## 3. Run the Homey app

{% hint style="info" %}
For sake of brevity, this guide will not cover what a Homey App is made off. If you are interested in what files and folders `homey app create` just created, you can read the [App guide](/the-basics/app).
{% endhint %}

Now you should have a very basic Homey App automatically generated by Homey CLI. In order to see this basic app at work let's run it on a Homey. First, make sure that Homey is either:

* directly connected to your PC/laptop over USB, or
* connected to the same Wi-Fi network as the PC/laptop you are working on.

From within your Homey App project directory, run the following command to start uploading your Homey App to Homey.

```
homey app run
```

{% hint style="info" %}
If this is the first time you are running a Homey App on Homey a browser window will open that will ask you to login with your Athom account.
{% endhint %}

After you are logged in Homey CLI presents you with a list of available Homeys connected to your Athom account in the command-line window. Select the Homey on which you want to run the Homey App and it will start uploading and running automatically.

While the Homey App is running it will print debug logging to the command-line window. In order to quit running the Homey App, press `CTRL + C` and it will be uninstalled.

## Useful commands

A number of commands which might be useful are listed below:

* Login with a different account using: `homey logout`/`homey login`
* Switch to a different Homey using: `homey select`
* Run a Homey App in development mode without keeping your command-line window open: `homey app install`
* [Publish a Homey App](/app-store/publishing) to the Homey App Store: `homey app publish`

If you want to learn about all other functionalities provided by Homey CLI use the `--help` flag to list all possible commands.

```
homey --help
```

To see sub-commands, you can type:

```
homey app --help
```

## What's Next

You're off to a great start, after creating and running your Homey App there is a lot more to discover. Homey Apps can offer a lot of functionality for Homey users, for example with [Flows](/the-basics/flow) and [Devices](/the-basics/devices). Continue reading The Basics to see what is possible and extend your Homey App to your liking.


# Homey CLI

Complete reference of every Homey CLI command, option, and workflow.

The [Homey command-line interface (CLI)](/the-basics/getting-started/homey-cli) is the tool you need for Homey App development. It scaffolds new apps, runs them against a Homey (locally in a Docker container or on the Homey itself), validates them against App Store requirements, and publishes them. Read the [getting started guide](/the-basics/getting-started) first if you have not installed the CLI yet.

This page is a full reference of every command, subcommand and option. Commands are grouped in three sections:

* [**App Commands**](#app-commands) — everything under `homey app …`, for building and shipping apps.
* [**Top-level Commands**](#top-level-commands) — direct access to the Homey API via `homey api …`.
* [**Additional Commands**](#additional-commands) — authentication, Homey selection, and other utilities.

At the bottom you will find a section on [**global options**](#global-options), [**shell completion**](#shell-completion), [**environment variables**](#environment-variables) and a [**troubleshooting**](#troubleshooting) checklist.

{% hint style="info" %}
All `homey app …` commands accept `--path <dir>` (or `-p`) to point at a Homey app that is not in the current working directory. When omitted, the CLI uses `process.cwd()`.
{% endhint %}

## App Commands

The Homey CLI has commands that help when developing a Homey app. These commands all start with `homey app` and expect a Homey app in the current working directory.

### Create a new Homey app

```bash
homey app create
```

Interactive wizard that scaffolds a new empty Homey app. Prompts you for the App ID, name, description, category, color, and language. Creates `app.json`, the `.homeycompose/` layout, `README.md`, locale files, and installs dependencies.

Read the [getting started guide](/the-basics/getting-started) for a walkthrough of your first app.

### Adding a new Driver

```bash
homey app driver create
```

Adds a new driver to your Homey app. Prompts you for the driver ID, display name, class, capabilities, and pairing method. Generates the driver folder under `drivers/<id>/` including `driver.js`, `device.js`, and `driver.compose.json`.

### Change the capabilities of a Driver

```bash
homey app driver capabilities
```

Interactive editor that lists every available Homey capability and lets you toggle which ones the driver exposes. Writes the result back to the driver's `driver.compose.json`.

### Add/update firmware updates of a Driver

```bash
homey app driver firmware --driver <path> --firmware <file> [--firmware <file>...]
```

Register a device firmware update against a driver. Supported for Zigbee and Z-Wave drivers.

| Option       | Type           | Description                                                                |
| ------------ | -------------- | -------------------------------------------------------------------------- |
| `--driver`   | string         | Path to the driver folder that the firmware update should be attached to.  |
| `--firmware` | string (array) | Path to a firmware file. Repeat to attach multiple firmware files at once. |

**Example**

```bash
homey app driver firmware \
  --driver ./drivers/my-plug \
  --firmware ./firmware/v1.2.3.bin
```

### Adding a new Flow card for a Driver

```bash
homey app driver flow
```

Interactive wizard for creating a Flow card scoped to a specific driver. Prompts you for the card type (trigger, condition, or action), title, tokens, and generates the card under `drivers/<id>/driver.flow.compose.json`.

### Adding a new Flow card

```bash
homey app flow create
```

Same wizard as above, but for app-level Flow cards (not tied to a driver). Writes the card to `.homeycompose/flow/<type>/<id>.json`.

### Adding a new Widget

```bash
homey app widget create
```

Interactive wizard that scaffolds a new [dashboard widget](/the-basics/widgets), including the HTML/CSS/JS files, `widget.compose.json`, and the light/dark preview images.

### Adding a new Discovery Strategy

```bash
homey app discovery create
```

Interactive wizard that adds a [discovery strategy](https://github.com/athombv/gitbook-apps/tree/master/guides/tools/discovery-mdns-sd.md) (mDNS-SD, SSDP, or MAC) to `.homeycompose/discovery/`.

### Install TypeScript utilities

```bash
homey app add-types
```

Installs the Homey Apps SDK type declarations and configures `jsconfig.json` / `tsconfig.json` so your IDE and TypeScript compiler can type-check your app. Read the [TypeScript guide](/guides/tools/typescript) for details.

### Add GitHub workflows

```bash
homey app add-github-workflows
```

Copies ready-made GitHub Actions into `.github/workflows/` for validating, versioning, and publishing your app on push. See [Automating within GitHub Actions](/app-store/publishing#automating-within-github-actions).

### Build a Homey app for publishing

```bash
homey app build [options]
```

Create a production build of your app. Compiles TypeScript (if applicable), runs Homey Compose, and produces the tarball that `install` and `publish` use.

| Option                 | Type   | Description                                                                                                        |
| ---------------------- | ------ | ------------------------------------------------------------------------------------------------------------------ |
| `--docker-socket-path` | string | Path to the Docker socket. Useful when Docker is running on a non-standard socket (Colima, Rancher Desktop, etc.). |
| `--find-links`         | string | Additional location to search for candidate Python package distributions (Python apps only).                       |

### Migrate to Homey Compose

```bash
homey app compose
```

Splits a legacy monolithic `app.json` into the `.homeycompose/` file layout. Existing files are preserved. Only useful for apps that predate Homey Compose.

### Validate a Homey app

```bash
homey app validate [--level debug|publish|verified] [options]
```

Validates the app manifest, assets, and compose files. `run`, `install`, and `publish` call this automatically.

| Option                 | Type   | Default   | Description                                                     |
| ---------------------- | ------ | --------- | --------------------------------------------------------------- |
| `--level`, `-l`        | string | `publish` | Validation strictness. See table below.                         |
| `--docker-socket-path` | string | —         | Path to the Docker socket.                                      |
| `--find-links`         | string | —         | Additional location to search for Python package distributions. |

| Level      | When to use                                                                                                                                                             |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `debug`    | During development. Optional fields such as `images`, `brandColor`, and `category` are not required.                                                                    |
| `publish`  | Required to publish to the Homey App Store for Homey Pro.                                                                                                               |
| `verified` | Required for verified developers and Homey Cloud. Adds requirements such as `platforms`, `connectivity`, and `support`. Applied by default when you are a verified dev. |

**Examples**

```bash
homey app validate
homey app validate --level verified
```

### Run a Homey app in development mode

```bash
homey app run [options]
```

Runs and debugs your app. By default it runs in a local Docker container that exposes the app to your selected Homey. For Homey Pro (2016—2019) the app is automatically uploaded to the Homey and run remotely. Console output streams to your terminal. Quitting (`Ctrl+C`) uninstalls the app from Homey.

| Option                 | Type    | Default  | Description                                                                                                                                  |
| ---------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `--clean`, `-c`        | boolean | `false`  | Delete all userdata, paired devices, and settings before running. Useful when testing pairing flows.                                         |
| `--remote`, `-r`       | boolean | `false`  | Force the app to run on the Homey instead of locally in Docker. Automatically enabled on Homey Pro (2016—2019).                              |
| `--skip-build`, `-s`   | boolean | `false`  | Skip the build step. Use only if you know the build output is already up-to-date.                                                            |
| `--link-modules`, `-l` | string  | `""`     | Comma-separated list of local Node.js modules to link into the runner. Docker mode only.                                                     |
| `--network`, `-n`      | string  | `bridge` | Docker network mode. Must match a name from `docker network ls`. Use `host` if your app needs LAN discovery from the host. Docker mode only. |
| `--docker-socket-path` | string  | —        | Path to the Docker socket.                                                                                                                   |
| `--find-links`         | string  | —        | Additional location to search for Python package distributions.                                                                              |

**Examples**

```bash
homey app run
homey app run --clean
homey app run --remote
homey app run --link-modules ../my-library,../another-library
homey app run --network host
```

### Install a Homey app

```bash
homey app install [options]
```

Builds the app and installs it on the currently selected Homey. Unlike `homey app run`, this leaves the app installed after the command exits and does not stream logs. Good for long-running tests.

| Option               | Type    | Default | Description                                                          |
| -------------------- | ------- | ------- | -------------------------------------------------------------------- |
| `--clean`, `-c`      | boolean | `false` | Delete all userdata, paired devices, and settings before installing. |
| `--skip-build`, `-s` | boolean | `false` | Skip the build step.                                                 |

### Open your app in Homey Developer Tools

```bash
homey app manage
```

Opens `https://tools.developer.homey.app/apps/app/<app-id>` in your default browser.

### Publish a Homey app to the Homey App Store

```bash
homey app publish [options]
```

Validates, builds, and uploads your app to the Homey App Store. You will be prompted to bump the version and add a changelog if you have not already. Read the [publishing guide](/app-store/publishing) for the full submission flow.

| Option                 | Type   | Description                                                     |
| ---------------------- | ------ | --------------------------------------------------------------- |
| `--docker-socket-path` | string | Path to the Docker socket.                                      |
| `--find-links`         | string | Additional location to search for Python package distributions. |

Both options are passed through to the internal build step (see [`homey app build`](#build-a-homey-app-for-publishing)). Most developers can ignore them.

### Update a Homey app's version

```bash
homey app version <next> [--changelog.<lang> "..."] [--commit]
```

Bumps `version` in `app.json`. Homey apps use [semver](https://semver.org/).

| Argument / Option    | Type              | Description                                                                                     |
| -------------------- | ----------------- | ----------------------------------------------------------------------------------------------- |
| `<next>`             | string (required) | `patch`, `minor`, `major`, or an explicit semver like `2.0.0`.                                  |
| `--changelog.<lang>` | string            | Changelog text for a specific language. Repeat to translate. Written to `.homeychangelog.json`. |
| `--commit`           | boolean           | Create a git commit and matching tag for the new version.                                       |

**Examples**

```bash
homey app version patch
homey app version minor --commit
homey app version 2.0.0 \
  --changelog.en "Added support for the Awesome Widget" \
  --changelog.nl "Ondersteuning voor de Awesome Widget toegevoegd"
```

### Translate your app with OpenAI

```bash
homey app translate [options]
```

Uses the OpenAI API to translate your app's `.json` fields and `README.txt` into every language your app targets. Requires `OPENAI_API_KEY` to be set (or passed with `--api-key`).

| Option        | Type   | Default            | Description                                                                          |
| ------------- | ------ | ------------------ | ------------------------------------------------------------------------------------ |
| `--languages` | string | app's target langs | Comma-separated list of target language codes (e.g. `nl,de,fr`).                     |
| `--api-key`   | string | `$OPENAI_API_KEY`  | OpenAI API key. Prefer setting the environment variable.                             |
| `--model`     | string | `gpt-4o`           | OpenAI model to use.                                                                 |
| `--file`      | string | —                  | Absolute path to a single file to translate. Useful when you only edited one string. |

{% hint style="warning" %}
AI translations vary in quality. Always review the diff before committing.
{% endhint %}

### Review your app with AI

```bash
homey app review [options]
```

Runs an AI review of your app against the [Homey App Store Guidelines](/app-store/guidelines) before you submit for certification. The reviewer analyzes `app.json`, driver metadata, and every image (app icon, driver images, widget previews). Returns a verdict of `approve`, `request_changes`, or `reject`, plus findings grouped by severity (`blocker`, `warning`, `suggestion`).

| Option            | Type    | Default         | Description                                                                                                             |
| ----------------- | ------- | --------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `--type`          | string  | `new`           | `new` for a first submission, `update` if the app is already live.                                                      |
| `--model`         | string  | Athom's default | Model in `<provider>/<model>` form. Supported providers: `openai`, `anthropic`. Any non-default model prints a warning. |
| `--json`          | boolean | `false`         | Emit machine-readable JSON instead of pretty output.                                                                    |
| `--verbose`, `-v` | boolean | `false`         | Print token counts, model info, and duration.                                                                           |

The command reads optional app-specific instructions from a `.homeyreview.md` file at the app root. Requires `OPENAI_API_KEY` (for OpenAI models) or `ANTHROPIC_API_KEY` (for Anthropic models). Exits with code 1 if the verdict is `reject`.

**Examples**

```bash
export OPENAI_API_KEY="sk-..."
homey app review
homey app review --type update
homey app review --json > review.json
homey app review --model anthropic/claude-opus-4-7 --verbose
```

### Open the App Store page of your app

```bash
homey app view
```

Opens `https://homey.app/a/<app-id>` in your default browser.

### Dependency utilities (Python apps)

```bash
homey app dependencies <install|add|remove|list>
```

Manages Python dependencies for [Python-based Homey apps](https://github.com/athombv/gitbook-apps/tree/master/guides/programming-languages/python.md). All subcommands accept `--find-links` and `--docker-socket-path`.

#### `homey app dependencies install`

```bash
homey app dependencies install
```

Installs libraries listed in the app's dependency file and pre-compiles them for distribution with the app.

#### `homey app dependencies add`

```bash
homey app dependencies add [dev] <package>[@<version>] [...]
```

Adds one or more libraries as a dependency. When run with the leading `dev` keyword, packages are added as development-only dependencies. Adding a package that is already installed updates its version constraint.

**Examples**

```bash
homey app dependencies add requests
homey app dependencies add "numpy>=1.26,<2.0"
homey app dependencies add dev pytest
```

#### `homey app dependencies remove`

```bash
homey app dependencies remove [dev] <package> [...]
```

Removes libraries. Use the leading `dev` keyword to remove from dev dependencies.

#### `homey app dependencies list`

```bash
homey app dependencies list
```

Prints all installed dependencies with their resolved versions.

## Top-level Commands

### Direct Homey API commands

```bash
homey api <subcommand>
```

Use `homey api` to inspect and call the Homey API directly. Subcommands:

* `homey api schema` — inspect available managers and operations.
* `homey api diagnose` — diagnose local discovery / connectivity.
* `homey api raw` — perform an arbitrary HTTP request against the Homey.
* `homey api <manager> <operation>` — call a manager method (e.g. `homey api devices open-device`). Manager commands are generated automatically from the Homey API schema.

#### `homey api schema`

```bash
homey api schema [--json] [--jq "<expr>"]
```

Prints a human-readable overview of every available API manager and its operations. With `--json` you get the raw schema, which you can filter with `--jq`.

**Examples**

```bash
homey api schema
homey api schema --json --jq '.managers | keys'
```

#### `homey api diagnose`

```bash
homey api diagnose [--homey-id <id>] [--json] [--jq "<expr>"]
```

Tries every discovery strategy (local address, mDNS, cloud tunnel, WebSocket relay) against the selected Homey and prints which ones work, how long they take to respond, and which one is used. Exits with code 0 if at least one strategy is available, 1 otherwise. Great when `homey app run` cannot reach your Homey.

| Option       | Description                                                  |
| ------------ | ------------------------------------------------------------ |
| `--homey-id` | Diagnose a cached Homey by ID instead of the selected Homey. |
| `--json`     | Output the diagnosis as JSON.                                |
| `--jq`       | Filter JSON output with a jq expression.                     |

#### `homey api raw`

```bash
homey api raw --path <api-path> [--method GET|POST|PUT|...] [options]
```

Aliases: `homey api call`, `homey api request`. Perform an arbitrary Homey API request. Useful for quick debugging, scripting, or exploring the API.

| Option           | Type    | Default | Description                                                                                            |
| ---------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------ |
| `--path`         | string  | —       | Required. Homey API path, must start with `/` (e.g. `/api/manager/system/`).                           |
| `--method`, `-X` | string  | `GET`   | HTTP method. Case-insensitive.                                                                         |
| `--header`, `-H` | string  | —       | Request header in `"name:value"` form. Repeatable.                                                     |
| `--body`         | string  | —       | Request body. Inline JSON, or `@path/to/file.json` to load from disk.                                  |
| `--request-json` | boolean | `true`  | Encode the request body as JSON. Disable for raw bodies.                                               |
| `--include`      | boolean | `false` | Print status line and response headers in addition to the body.                                        |
| `--verbose`      | boolean | `false` | Print the resolved request URL, method, headers, and timing to stderr. Sensitive headers are redacted. |
| `--token`        | string  | —       | Use a session token instead of the selected Homey. Requires `--address` or `--homey-id`.               |
| `--address`      | string  | —       | Homey base URL for token mode (e.g. `http://192.168.1.100`).                                           |
| `--homey-id`     | string  | —       | Target a cached Homey by ID.                                                                           |
| `--timeout`      | number  | `30000` | Request timeout in milliseconds.                                                                       |
| `--json`         | boolean | `false` | Force JSON output even when the response is a plain string.                                            |
| `--jq`           | string  | —       | Filter JSON output with a jq expression.                                                               |

**Examples**

```bash
homey api raw --path /api/manager/system/
homey api raw --path /api/manager/system/ --jq '.value.homeyVersion'

homey api raw \
  -X POST \
  --path /api/manager/flow/flow \
  --body '{"name":"Test flow"}'

homey api raw \
  -X POST \
  --path /api/manager/flow/flow \
  --body @./flow.json --verbose
```

#### `homey api <manager> <operation>`

Every manager in the Homey API is exposed as its own subcommand. For example:

```bash
homey api devices open-device --id <device-id>
```

Run `homey api --help` for the list of available managers, or `homey api schema` to inspect operations. Each manager command inherits `--homey-id`.

## Additional Commands

### Login with an Athom account

```bash
homey login
```

Opens the OAuth2 dialog in your default browser and stores the resulting session in `~/.homey/`. Required before `homey app run`, `install`, or `publish`. For CI/CD, set `HOMEY_PAT` instead of running `homey login`.

### Logout the current user

```bash
homey logout
```

Clears the stored session from `~/.homey/`.

### Show the current logged-in user

```bash
homey whoami [--json] [--jq "<expr>"]
```

Prints the first name, last name, and email of the authenticated Athom user. Verified developers are marked as such.

### List your Homeys

```bash
homey list [--json] [--jq "<expr>"]
```

Lists every Homey linked to the authenticated account with ID, name, platform, version, region, and role. Sorted by state (online Homeys first).

**Example**

```bash
homey list --json --jq '.[].name'
```

### Select a Homey

```bash
homey select [--id <id> | --name <name>]
```

Sets the active Homey for `homey app run`, `homey app install`, and every `homey api` command. Without arguments, an interactive picker is shown. Provide `--id` or `--name` to select non-interactively (useful in scripts).

#### `homey select current`

```bash
homey select current [--json] [--jq "<expr>"]
```

Prints the currently selected Homey. Exits with a helpful message when nothing is selected.

### Unselect the active Homey

```bash
homey unselect
```

Clears the currently selected Homey so subsequent commands will prompt for one.

### Open Homey Developer Tools

```bash
homey tools
```

Opens `https://tools.developer.homey.app` in your default browser.

### Open the Homey Apps SDK documentation

```bash
homey docs
```

Opens `https://apps.developer.homey.app` in your default browser.

## Global options

Every command supports these:

| Option            | Description                                                                               |
| ----------------- | ----------------------------------------------------------------------------------------- |
| `--help`          | Show help for the current command, including all options and subcommands.                 |
| `--version`, `-v` | Print the installed CLI version.                                                          |
| `--path`, `-p`    | Available on every `homey app …` command. Points at the app directory. Defaults to `cwd`. |

Commands that produce structured output additionally support:

| Option          | Description                                                                         |
| --------------- | ----------------------------------------------------------------------------------- |
| `--json`        | Emit machine-readable JSON. Combine with your own parsing or with `--jq`.           |
| `--jq "<expr>"` | Filter the JSON output with a [jq](https://jqlang.org/) expression before printing. |

Commands that currently support `--json`/`--jq`: `homey whoami`, `homey list`, `homey select current`, `homey api schema`, `homey api diagnose`, `homey api raw`, and (partially) `homey app review`.

## Shell completion

The CLI ships tab-completion for `bash`, `zsh`, and `fish` via yargs.

```bash
# Print the completion script for your current shell
homey completion
```

Add the output to your shell startup file:

{% tabs %}
{% tab title="zsh" %}

```bash
homey completion >> ~/.zshrc
source ~/.zshrc
```

{% endtab %}

{% tab title="bash" %}

```bash
homey completion >> ~/.bashrc
source ~/.bashrc
```

{% endtab %}

{% tab title="fish" %}

```bash
homey completion > ~/.config/fish/completions/homey.fish
```

{% endtab %}
{% endtabs %}

After sourcing, `homey <TAB>` completes commands, subcommands, and options. `homey api <TAB>` additionally completes manager names discovered from the Homey API.

## Environment variables

| Variable            | Used by                                   | Description                                                                        |
| ------------------- | ----------------------------------------- | ---------------------------------------------------------------------------------- |
| `HOMEY_PAT`         | Every command that talks to Athom Cloud   | Personal Access Token. Bypasses the interactive login flow. Recommended for CI/CD. |
| `OPENAI_API_KEY`    | `homey app translate`, `homey app review` | OpenAI API key. Required unless you pass `--api-key`.                              |
| `ANTHROPIC_API_KEY` | `homey app review`                        | Anthropic API key. Required when `--model anthropic/…` is used.                    |

Persistent state (session tokens, cached Homeys, selected Homey) lives in `~/.homey/`. Delete this directory to fully reset the CLI.

## Troubleshooting

**"No Homey is currently selected."**\
Run `homey select` (interactive) or `homey select --name "<homey>"`. Check the current selection with `homey select current`.

**"Cannot connect to the Docker daemon."**\
`homey app run`, `build`, `validate`, and `publish` require Docker to be running on Homey Pro (Early 2023) and later. Start Docker Desktop / Colima / Rancher Desktop. If Docker is running on a non-default socket, pass `--docker-socket-path <path>`.

**Cannot reach the Homey from Docker.**\
Try `homey api diagnose` to see which discovery strategies work. Running with `homey app run --network host` (macOS/Linux) often helps when local mDNS discovery is required.

**Homey Pro (2016—2019) cannot use Docker.**\
Pass `--remote` (or let the CLI do it automatically) to upload and run the app directly on the Homey. Console output still streams to your terminal but rebuilds are slower than in Docker mode.

**Login flow doesn't open a browser.**\
Copy the URL from the terminal into your browser manually.

**`homey app publish` rejects the app because of validation errors.**\
Run `homey app validate --level publish` (or `--level verified` for verified developers) locally and fix each error. See the [publishing guide](/app-store/publishing#requirements) for the full requirements matrix.

**`homey app review` fails with a missing-key error.**\
Set `OPENAI_API_KEY` (for OpenAI models) or `ANTHROPIC_API_KEY` (for Anthropic models) in your shell before running the command.

**Reset the CLI to a clean state.**\
Remove `~/.homey/` to clear all sessions, cached Homeys, and the current selection.


# App

The essential concepts of a Homey App.

On this page we will look at what the various files in the Homey app folder are for and what essential concepts you should be aware of to start developing your Homey app.

Apps are able to use all of Homey's capabilities and thus can use the following technologies:

* [Wi-Fi](/wireless/wi-fi)
* [Bluetooth LE](/wireless/bluetooth)
* [Z-Wave](/wireless/z-wave)
* [Zigbee](/wireless/zigbee)
* [433 Mhz](/wireless/rf-433mhz-868mhz)
* [Infrared](/wireless/infrared)

Most Apps use only one wireless technology but it is possible to use them all at the same time in a single Homey App. Regardless of the wireless technologies used in your Homey app most of the concepts are the same. Every Homey app has the same basic file structure:

{% tabs %}
{% tab title="JavaScript" %}

```
com.athom.example/
├─ .homeycompose/
│ ├─ app.json
│ └─ ...
├─ assets/
│ ├─ icon.svg
│ └─ images
│   ├─ small.png
│   ├─ large.png
│   └─ xlarge.png
├─ drivers/
│ ├─ my_driver/
│ │ ├─ assets/
│ │ │ ├─ icon.svg
│ │ │ └─ images/
│ │ │   ├─ small.png
│ │ │   ├─ large.png
│ │ │   └─ xlarge.png
│ │ ├─ device.js
│ │ └─ driver.js
│ └─ ... 
├─ locales/
│ ├─ en.json
│ └─ nl.json
├─ settings/
│ └─ index.html
├─ api.js
├─ app.js
├─ app.json
├─ env.json
└─ README.txt
```

{% endtab %}

{% tab title="TypeScript" %}

```
com.athom.example/
├─ .homeycompose/
│ ├─ app.json
│ └─ ...
├─ assets/
│ ├─ icon.svg
│ └─ images
│   ├─ small.png
│   ├─ large.png
│   └─ xlarge.png
├─ drivers/
│ ├─ my_driver/
│ │ ├─ assets/
│ │ │ ├─ icon.svg
│ │ │ └─ images/
│ │ │   ├─ small.png
│ │ │   ├─ large.png
│ │ │   └─ xlarge.png
│ │ ├─ device.mts
│ │ └─ driver.mts
│ └─ ... 
├─ locales/
│ ├─ en.json
│ └─ nl.json
├─ settings/
│ └─ index.html
├─ api.mts
├─ app.mts
├─ app.json
├─ env.json
└─ README.txt
```

{% endtab %}

{% tab title="Python" %}

```
com.athom.example/
├─ .homeycompose/
│ ├─ app.json
│ └─ ...
├─ .python_cache/
│ └─ ... 
├─ assets/
│ ├─ icon.svg
│ └─ images
│   ├─ small.png
│   ├─ large.png
│   └─ xlarge.png
├─ drivers/
│ ├─ my_driver/
│ │ ├─ assets/
│ │ │ ├─ icon.svg
│ │ │ └─ images/
│ │ │   ├─ small.png
│ │ │   ├─ large.png
│ │ │   └─ xlarge.png
│ │ ├─ device.py
│ │ └─ driver.py
│ └─ ... 
├─ locales/
│ ├─ en.json
│ └─ nl.json
├─ settings/
│ └─ index.html
├─ api.py
├─ app.py
├─ app.json
├─ env.json
└─ README.txt
```

{% endtab %}
{% endtabs %}

## App Manifest

The `/app.json` file is the complete app manifest, it tells Homey what your app does. When you run or publish your app this file is generated from the various `*.compose.json` files and the JSON files in the `/.homeycompose/` folder. The generation of your app manifest is referred to as Homey Compose and it supports some additional advanced features such as templating. All the various compose files will be explained in the rest of the documentation.

All basic information about your app is configured in the `/.homeycompose/app.json` file. You can read the [App Manifest documentation](/the-basics/app/manifest) to learn more about this file.

{% hint style="warning" %}
Because the `/app.json` file gets generated from the various Homey Compose files you should never manually edit it. You should edit the `*.compose.json` and JSON files in the `/.homeycompose/` folder instead.
{% endhint %}

## App Class

{% tabs %}
{% tab title="JavaScript" %}
In the `/app.js` file you can create an `App` class. This class gets instantiated once, when your app started. This is a great place to put logic that is shared throughout your app, and it is sometimes necessary for Flow cards and other resources that you only want to setup once.

The app instance can be accessed from your `Driver` and `Device` classes through `this.homey.app`.

{% code title="/app.js" %}

```javascript
const Homey = require('homey');
const ApiClient = require('your-external-api-client');

class App extends Homey.App {
  async onInit() {
    // create an ApiClient once when the app is started
    this.client = new ApiClient();
  }
}

module.exports = App;
```

{% endcode %}

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require('homey');

class Device extends Homey.Device {
  async onInit() {
    // access the ApiClient through the App instance
    const data = await this.homey.app.client.getData();
  }
}

module.exports = Device;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
In the `/app.mts` file you can create an `App` class. This class gets instantiated once, when your app started. This is a great place to put logic that is shared throughout your app, and it is sometimes necessary for Flow cards and other resources that you only want to setup once.

The app instance can be accessed from your `Driver` and `Device` classes through `this.homey.app`.

{% code title="/app.mts" %}

```mts
import Homey from 'homey';
import ApiClient from 'your-external-api-client';

export default class App extends Homey.App {
  client?: ApiClient;

  async onInit(): Promise<void> {
    // create an ApiClient once when the app is started
    this.client = new ApiClient();
  }
}

```

{% endcode %}

{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey from 'homey';
import type App from '../../app.mjs';

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    // access the ApiClient through the App instance
    const data = await (this.homey.app as App).client?.getData();
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
In the `/app.py` file you can create an `App` class. This class gets instantiated once, when your app started. This is a great place to put logic that is shared throughout your app, and it is sometimes necessary for Flow cards and other resources that you only want to setup once.

The app instance can be accessed from your `Driver` and `Device` classes through `self.homey.app`.

{% code title="/app.py" %}

```python
from homey import app

from your_external_api_client import ApiClient


class App(app.App):
    client: ApiClient

    async def on_init(self) -> None:
        # create an ApiClient once when the app is started
        self.client = ApiClient()


homey_export = App

```

{% endcode %}

{% code title="/drivers/\<driver\_id>/device.py" %}

```python
from typing import cast

from homey import device

from ...app import App


class Device(device.Device):
    async def on_init(self) -> None:
        # access the ApiClient through the App instance
        data = await cast(App, self.homey.app).client.get_data()


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

## App API

Homey apps can expose an API that other devices can talk to. It is even possible for Homey apps to talk to each other by exposing an API. The API implementation in the root folder of your app contains the handlers for all the API endpoints. You can read about exposing an API from your App in the [Web API guide](/advanced/web-api).

## Assets

In the `/assets/` folder at the root of your app you can put the assets of the app. These are the app icon `icon.svg` and the app images that will be shown in the Homey App Store. Read the [App Store guidelines](/app-store/guidelines#1-4-images) for more information about the icon and images.

## Drivers and Devices

The `/drivers/` folder contains all the drivers of your app. The drivers are the parts of a Homey app that allow users to add and then control devices. This folder should only contain other folders, it is common for those folders to be named after the product code of the device they are implementing. You can learn more by reading the [Devices documentation](/the-basics/devices).

## Locales

Translation files are kept in the `/locales/` directory. You can learn more about these files by reading the [internationalization documentation](/the-basics/app/internationalization).

## Readme

The `/README.txt` file in the root of your app contains the long-form description of your app. This will be shown in the Homey App Store when a user is browsing your app. Read the [App Store guidelines](/app-store/guidelines#1-3-readme) for more information about the readme.

{% hint style="info" %}
You can supply translated versions of the readme by naming the file `README.<languagecode>.txt`, for example `README.nl.txt` would contain the Dutch translation of the readme. You can find the supported languages in the [internationalization documentation](/the-basics/app/internationalization#supported-language-codes).
{% endhint %}

## Settings

{% tabs %}
{% tab title="JavaScript" %}
In your Homey app you can save settings that are persistent across reboots. These settings can be accessed from anywhere in your app through [`ManagerSettings`](https://apps-sdk-v3.developer.homey.app/ManagerSettings.html).

{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  async onInit() {
    const username = this.homey.settings.get('username');
    // ...
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
In your Homey app you can save settings that are persistent across reboots. These settings can be accessed from anywhere in your app through [`ManagerSettings`](https://apps-sdk-v3.developer.homey.app/ManagerSettings.html).

{% code title="/app.mts" %}

```mts
import Homey from 'homey';

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    const username = this.homey.settings.get('username');
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
In your Homey app you can save settings that are persistent across reboots. These settings can be accessed from anywhere in your app through [`ManagerSettings`](https://python-apps-sdk-v3.developer.homey.app/manager/settings.html).

{% code title="/app.py" %}

```python
from homey import app


class App(app.App):
    async def on_init(self) -> None:
        username = self.homey.settings.get("username")


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}

You can also create a page where users can update the App Settings by creating an `index.html` file in the `/settings/` folder. Since drivers have their own settings, most apps shouldn't need any App settings. Read more about how and when to use app settings in the [App Settings guide](/advanced/custom-views/app-settings).

## Environment

The `/env.json` file in the root of your app contains the environment variables of your app. Since this file is usually used to store secret keys it should be kept on your computer. If you use Git the `/env.json` file should therefore be added to the `/.gitignore` file. In this file you can store information that should not be public, for example the OAuth2 tokens your app uses to connect to a cloud service.

{% code title="/env.json" %}

```javascript
{
  "CLIENT_ID": "12345abcde",
  "CLIENT_SECRET": "182hr2389r824ilikepie1302r0832"
}
```

{% endcode %}

The variables are available anywhere in your app under `Homey.env.CLIENT_ID`, `Homey.env.CLIENT_SECRET`, etc. Make sure they are uppercase, and that their value is a string.

{% tabs %}
{% tab title="JavaScript" %}

```javascript
const Homey = require('homey');

const CLIENT_ID = Homey.env.CLIENT_ID;
const CLIENT_SECRET = Homey.env.CLIENT_SECRET;
```

{% endtab %}

{% tab title="TypeScript" %}

```mts
import Homey from 'homey';

const CLIENT_ID = Homey.env.CLIENT_ID;
const CLIENT_SECRET = Homey.env.CLIENT_SECRET;

```

{% endtab %}

{% tab title="Python" %}

```python
from homey.homey import Homey

CLIENT_ID = Homey.env.get("CLIENT_ID")
CLIENT_SECRET = Homey.env.get("CLIENT_SECRET")

```

{% endtab %}
{% endtabs %}

{% hint style="danger" %}
These variables are stored on Homey, and in theory should not be readable by anyone. However, make sure that these variables alone do not provide access to private resources.
{% endhint %}

## Node.js

As of Homey v12.9.0, all Homey platforms run apps on Node.js v22 (see [Node.js 22 upgrade guide](/upgrade-guides/node-22)). The table below lists the Node.js versions used by earlier Homey software versions across different platforms.

| Platform               | Version Range          | Node.js Version |
| ---------------------- | ---------------------- | --------------- |
| Homey Pro (2016–2019)  | < v7.4.0               | v12             |
| Homey Pro (2016–2019)  | >= v7.4.0 && < v12.9.0 | v16             |
| Homey Pro (2016–2019)  | >= v12.9.0             | v22             |
| Homey Pro (Early 2023) | < v12.9.0              | v18             |
| Homey Pro (Early 2023) | >= v12.9.0             | v22             |
| Homey Pro (mini)       | < v12.9.0              | v18             |
| Homey Pro (mini)       | >= v12.9.0             | v22             |
| Homey Cloud            | >= v12.9.0             | v22             |

> Note: Homey Cloud apps migrate to Node.js 22 only after you publish a new version after December 2nd, 2025. Earlier versions will continue running on the previous Node.js version.

## Python

The Python version running on Homey platforms is the latest available full release, currently `3.14`. Once a new version is released, apps using the previous version do not immediately stop working, but they should be updated to guarantee future compatibility.

### Dependencies

To ensure Python dependencies are compatible with all Homey platforms that support the Python runtime, the dependencies of you app should be managed through the Homey CLI. It allows for [adding](/the-basics/getting-started/homey-cli#add-dependencies-to-a-homey-app), [removing](/the-basics/getting-started/homey-cli#remove-dependencies-from-a-homey-app) or [installing already defined](/the-basics/getting-started/homey-cli#install-the-dependencies-of-a-homey-app) dependencies. Using these commands will pre-compile the dependencies so they can be bundled with the app for installation and publishing. The pre-compiled environments are stored in `.python_cache` in your project folder.

### Typing

To add static type-checking to your Python project, or just add better suggestions to your IDE, you can install the [homey-stubs](https://pypi.org/project/homey-stubs/) package in a project [virtual environment](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/#create-and-use-virtual-environments), or globally if you want it to be available for all your projects.

You can install the package with a type-checker globally using the following command.

```bash
python -m pip install homey-stubs pyright
```

If you use `pyproject.toml` and want to disable [pyright](https://github.com/microsoft/pyright) warnings about missing sources for the `homey` module you can add the following to your pyproject.toml.

```toml
[tool.pyright]
reportMissingModuleSource = 'none'
```

## Homey Ignore

The `/.homeyignore` file in the root of your app can be used to prevent certain files or folders from being included in your Homey App when publishing. It works in the same way as `.gitignore`. By default all files in the app directory will be included when publishing your Homey App. `/.homeyignore` is useful in case you want to commit documentation, designs, images, etc. to your version control system but not include it in your Homey App when publishing.

{% code title="/.homeyignore" %}

```javascript
comments.txt
docs/*
```

{% endcode %}


# Manifest

The basic properties of the App Manifest.

The App Manifest contains all metadata for your app. It specifies Flow Cards, Drivers, etc.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/.homeycompose/app.json" %}

```json
{
  "id": "my.company.example",
  "version": "1.0.0",
  "compatibility": ">=5.0.0",
  "runtime": "nodejs",
  "platforms": ["local", "cloud"],
  "sdk": 3,
  "brandColor": "#FF0000",
  "name": { "en": "My App" },
  "description": { "en": "Adds support for Example devices." },
  "category": "lights",
  "tags": { "en": ["example"] },
  "images": {
    "small": "/assets/images/small.png",
    "large": "/assets/images/large.png",
    "xlarge": "/assets/images/xlarge.png"
  },
  "permissions": ["homey:manager:api"]
  "author": {
    "email": "john@doe.com",
    "name": "John Doe"
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/.homeycompose/app.json" %}

```json
{
  "id": "my.company.example",
  "version": "1.0.0",
  "compatibility": ">=5.0.0",
  "runtime": "nodejs",
  "platforms": ["local", "cloud"],
  "sdk": 3,
  "brandColor": "#FF0000",
  "name": { "en": "My App" },
  "description": { "en": "Adds support for Example devices." },
  "category": "lights",
  "tags": { "en": ["example"] },
  "images": {
    "small": "/assets/images/small.png",
    "large": "/assets/images/large.png",
    "xlarge": "/assets/images/xlarge.png"
  },
  "permissions": ["homey:manager:api"]
  "author": {
    "email": "john@doe.com",
    "name": "John Doe"
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/.homeycompose/app.json" %}

```json
{
  "id": "my.company.example",
  "version": "1.0.0",
  "compatibility": ">=13.0.0",
  "runtime": "python",
  "pythonVersion": "3.14",
  "pythonDependencies": [],
  "platforms": ["local", "cloud"],
  "sdk": 3,
  "brandColor": "#FF0000",
  "name": { "en": "My App" },
  "description": { "en": "Adds support for Example devices." },
  "category": "lights",
  "tags": { "en": ["example"] },
  "images": {
    "small": "/assets/images/small.png",
    "large": "/assets/images/large.png",
    "xlarge": "/assets/images/xlarge.png"
  },
  "permissions": ["homey:manager:api"]
  "author": {
    "email": "john@doe.com",
    "name": "John Doe"
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Properties

<table data-header-hidden><thead><tr><th width="213">Name</th><th>Description</th></tr></thead><tbody><tr><td>Name</td><td>Description</td></tr><tr><td><code>id</code></td><td>The ID of your app. This is a reversed domain-name.</td></tr><tr><td><code>version</code></td><td>A <a href="http://semver.org">Semantic Version</a> of your app. Note that pre-release versions (e.g. <code>1.0.0-rc.1</code>) are not allowed.</td></tr><tr><td><code>compatibility</code></td><td>A <a href="http://semver.org">Semantic Version</a> indicating a range of Homey versions your app is compatible with. Use at least <code>"compatibility": ">=5.0.0"</code> (larger or equal than v5.0.0).</td></tr><tr><td><code>runtime</code></td><td>The runtime the app is written for. Allowed runtimes are <code>nodejs</code> and <code>python</code>.</td></tr><tr><td><code>platforms</code></td><td>An array containing the app's supported platforms. Read the <a href="/pages/-MizFyJ9gakIdl0JXQN0">Homey Cloud</a> guide for more information.</td></tr><tr><td><code>sdk</code></td><td>The SDK level of your app. Should be <code>3</code>.</td></tr><tr><td><code>brandColor</code></td><td>A HEX string for the app's brand color. The brand color is not allowed to be very bright.</td></tr><tr><td><code>name</code></td><td>A <a href="/pages/-MWdBAdwDRttE0KkWl48">translation object</a> with the name of your app.</td></tr><tr><td><code>description</code></td><td>A <a href="/pages/-MWdBAdwDRttE0KkWl48">translation object</a> with the description (oneliner) of your app.</td></tr><tr><td><code>category</code></td><td>A string with the Homey App Store category. Allowed categories are: <code>lights</code>, <code>video</code>, <code>music</code>, <code>appliances</code>, <code>security</code>, <code>climate</code>, <code>tools</code>, <code>internet</code>, <code>localization</code>, <code>energy</code>.</td></tr><tr><td><code>tags</code></td><td>A <a href="/pages/-MWdBAdwDRttE0KkWl48">translation object</a> with searchable tags for the App Store.</td></tr><tr><td><code>images</code></td><td>An object containing two paths, <code>small</code> and <code>large</code>, and optionally a third: <code>xlarge</code>.</td></tr><tr><td><code>permissions</code></td><td>An array of <a href="/pages/-MXCDM7-iF1CXO1HD7FX">Permissions</a>.</td></tr><tr><td><code>author</code></td><td>An object indicating the author of the app. The field <code>author.name</code> is required, <code>author.email</code> is optional.</td></tr></tbody></table>

## Python properties

The following app manifest properties are only required for apps using the Python runtime.

### Python version

A string with the Python version the app was developed for. Currently the only supported version is `3.14`.

```jsonc
{
  "id": "com.athom.example",
  // ...
  "runtime": "python",
  "pythonVersion": "3.14",
}
```

{% hint style="warning" %}
If the given version becomes outdated with the version(s) available on the Homey platforms, a newer version may be used to run the Homey app, which may result in incompatibility. When a new full-release Python version becomes available apps have about a year to update to the new version.
{% endhint %}

### Python dependencies

To keep track of the dependencies of the Homey app, and to allow for checking compatibility with Homey platforms, any libraries added as a dependency through the CLI will be stored in the app manifest.

```jsonc
{
  "id": "com.athom.example",
  // ...
  "runtime": "python",
  "pythonDependencies": [
    "aiohttp>=3.13"
  ],
}
```

{% hint style="danger" %}
Editing the dependencies in the app manifest without running `homey app dependencies install` results in the app being bundled with outdated libraries.
{% endhint %}

{% hint style="warning" %}
Installing dependencies through other means than `homey app dependencies install` results in those dependencies missing in the bundled app. Always test your app by installing it or running it remotely if you have changed the dependencies, in order to see whether they are bundled correctly.
{% endhint %}

## Additional properties

The following app manifest properties are optional but can be used to document extra information about your app. Most of this information will be presented to users in the Homey App Store.

### Platform Local Required Features

An array of required features (`nfc`, `ledring`, `speaker` or `matter`) for platform `local`. Adding a required feature will make the app uninstallable on Homey Pros that do not have all the listed features. So only use this when the app will only function with the respective feature.

```jsonc
{
  "id": "com.athom.example",
  // ...
  "platformLocalRequiredFeatures": [ "nfc", "speaker", "ledring" ]
}
```

### Contributors

An object with keys `developers` and `translators`. Each object in the `developers` or `translators` array must contain a `name` property.

{% code title="/.homeycompose/app.json" %}

```jsonc
{
  "id": "com.athom.example",
  // ...
  "contributors": {
    "developers": [
      {
        "name": "Alice the Wild"
      }
    ],
    "translators": [
      {
        "name": "Klemens Kohlmann"
      }
    ]
  }
}
```

{% endcode %}

### Contributing

Perhaps some users want to show their appreciation for your hard work in the form of a small donation.

When adding one or more donation options to your app's manifest, a *Donate* button will appear on your app's page.

![](/files/CSr4VV8ZxsnszkezpYxa)

{% hint style="info" %}
Donate buttons are only visible for non-verified developers.
{% endhint %}

{% code title="/.homeycompose/app.json" %}

```jsonc
{
  "id": "com.athom.example",
  // ...
  "contributing": {
    "donate": {
      "paypal": {
        // Will link to https://paypal.me/my_paypal_username
        "username": "my_paypal_username"
      },
      "bunq": {
        // Will link to https://bunq.me/my_bunq_username
        "username": "my_bunq_username"
       },
       "patreon": {
         // Will link to https://www.patreon.com/my_patreon_username
         "username": "my_patreon_username"
       },
       "githubSponsors": {
         // Will link to https://github.com/sponsors/my_github_username
         "username": "my_github_username"
       }
    }
  }
}
```

{% endcode %}

### Bugs

Users can come across an issue in your app. Provide them with a bug URL, so they can quickly share their findings with you. An object with a property `url` that contains a link to your public bug/issue tracker.

{% code title="/.homeycompose/app.json" %}

```jsonc
{
  "id": "com.athom.example",
  // ...
  "bugs": {
    "url": "https://bitbucket.org/athom/com.athom.myapp/issues"
  }
}
```

{% endcode %}

### Homey Community Topic

The [Homey Community](https://community.athom.com/) is a great place to share additional information about your app with your users and fellow developers. When the ID of the topic is provided, this shows a link on the App Store. You can get the ID from the topic's URL.

{% code title="/.homeycompose/app.json" %}

```jsonc
{
  "id": "com.athom.example",
  // ...
  "homeyCommunityTopicId": 1234
}
```

{% endcode %}

### Source

If you would like to, you can add a link to your source code to allow others to view your code and maybe even submit a Pull Request. A string, starting with `https://`.

{% code title="/.homeycompose/app.json" %}

```jsonc
{
  "id": "com.athom.example",
  // ...
  "source": "https://github.com/athombv/com.athom.myapp"
}
```

{% endcode %}

### Homepage

Share your company, brand or personal page with users to give them some more insight. A string, starting with `https://`.

{% code title="/.homeycompose/app.json" %}

```jsonc
{
  "id": "com.athom.example",
  // ...
  "homepage": "https://homey.app"
}
```

{% endcode %}

### Support

Sometimes your users need some help or might have some questions about your app. Consider offering them support, just in case they need it. Add a URL or e-mail address to the app manifest. A support URL is mandatory for Verified Developers. A string, starting with `https://` or `mailto:`.

{% code title="/.homeycompose/app.json" %}

```jsonc
{
  "id": "com.athom.example",
  // ...
  "support": "mailto:support@homey.app"
}
```

{% endcode %}


# Internationalization

Homey Apps support internationalization so that users can use your app in their native language.

Homey Apps can store translated strings as JSON files in the `/locales/` folder with the language code as filename. See these two example translation files for English and Dutch:

{% code title="/locales/en.json" %}

```javascript
{
  "title": "Hello World",
  "greeting": "Hello, __name__",
  "settings": {
    "title": "My Title",
    "intro": "This is an example page."
  },
  "pair": {
    "press_button": "Press the `pair` button on your device."
  }
}
```

{% endcode %}

{% code title="/locales/nl.json" %}

```javascript
{
  "title": "Hallo Wereld",
  "greeting": "Hallo, __name__",
  "settings": {
    "title": "Mijn Titel",
    "intro": "Dit is een voorbeeld pagina."
  },
  "pair": {
    "press_button": "Druk op de `pair` knop op jou apparaat."
  }
}
```

{% endcode %}

These files define translations with the ID's: `title`, `greeting`, `settings.title`, `settings.intro`, and `pair.press_button`. If a translated string with a certain ID is not present in the locale file for the users locale Homey will fall-back to English.

{% hint style="info" %}
Your app's `README.txt` can also be translated by creating additional `README.<languagecode>.txt` files. For example`README.nl.txt` would contain the Dutch translation of the readme.
{% endhint %}

## The translation object

In addition to the translation files you can define the translations directly inside the [App Manifest](/the-basics/app/manifest). This is done by defining an object with the language code as keys and the translations as the values.

```javascript
"title": {
  "en": "Hello, World!",
  "nl": "Hallo, wereld!"
}
```

You can also input a single string, when translation isn't necessary:

```javascript
"title": "°C"
```

{% hint style="info" %}
Always have at least an `en` translation! This is what Homey will fall-back on when the user language can't be found.
{% endhint %}

## Temperature conversion

Your Homey app should always use Celsius internally, Homey will automatically converts capability values from Celsius to Fahrenheit. For custom capabilities make sure the `"unit"` is set to `"°C"` to let Homey know that this value should be converted.

## Supported Languages

The following languages are currently supported for any Homey app.

* `en`: English
* `nl`: Dutch
* `de`: German
* `fr`: French
* `it`: Italian
* `sv`: Swedish
* `no`: Norwegian
* `es`: Spanish
* `da`: Danish
* `ru`: Russian
* `pl`: Polish
* `ko`: South Korean
* `ar:` Arabic

## Translating a string from your app

In your App, Drivers or Devices you can call the translation method on their Homey instance with the identifier of the translated string to get the translation. In the following example we define a translation with the ID `title` and use the translation in our `App` class.

{% code title="/locales/en.json" %}

```json
{
  "title": "Hello, World!"
}
```

{% endcode %}

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  onInit() {
    console.log(this.homey.__("title")); // "Hello, World!"
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/app.mts" %}

```mts
import Homey from 'homey';

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    console.log(this.homey.__('title')); // "Hello, World!"
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/app.py" %}

```python
from homey import app


class App(app.App):
    async def on_init(self) -> None:
        print(self.homey.translate("title"))  # "Hello, World!"


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}

You can also use variables in your translations. In the translated string variables are surrounded by two underscores (`__`). To get the translated string with variables you should call the translation method with the values that you want to use for those variables.

{% code title="/locales/en.json" %}

```json
{
  "greeting": "Hello, __name__!"
}
```

{% endcode %}

{% tabs fullWidth="false" %}
{% tab title="JavaScript" %}
{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  onInit() {
    console.log(this.homey.__("greeting", { name: "Dave" })); // "Hello, Dave!"
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/app.mts" %}

```mts
import Homey from 'homey';

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    console.log(this.homey.__('greeting', { name: 'Dave' })); // "Hello, Dave!"
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/app.py" %}

```python
from homey import app


class App(app.App):
    async def on_init(self) -> None:
        print(self.homey.translate("title", name="Dave"))  # "Hello, Dave!"


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}

## Translating a string in Custom views

Within your custom views, you can also use translations. For example:

{% code title="/locales/en.json" %}

```javascript
{
  "settings": {
    "title": "My Title",
    "intro": "This is an example page."
  }
}
```

{% endcode %}

{% code title="/settings/index.html" %}

```markup
<header class="homey-header">
  <h1 class="homey-title" data-i18n="settings.title">
    <!-- "My Title" will be placed here -->
  </h1>
  <p class="homey-subtitle" data-i18n="settings.intro">
    <!-- "This is an example page." will be placed here -->
  </p>
</header>
```

{% endcode %}

It is also possible to get translated strings with JavaScript:

{% code title="/settings/index.html" %}

```markup
<script type="application/javascript">
  function onHomeyReady(Homey) {
    alert(Homey.__("settings.title")); // will alert "Settings page title"
  }
</script>
```

{% endcode %}

To lean more about custom views you can read to the [custom views guide](/advanced/custom-views).

## Right-to-Left (RTL) and Arabic Language Support

Homey supports Right-to-Left (RTL) languages, including Arabic (ar). When an RTL language is active, the visual flow of the interface is mirrored, affecting layout direction, alignment, and spacing.

### **Built-in Pairing Views**

All built-in pairing views provided by the Homey App SDK fully support RTL out of the box.

If your app uses these pairing views, RTL layout behavior is handled automatically by Homey. To fully support Arabic and other RTL languages, make sure your app also includes the appropriate translations so the interface content is properly localized.

### **Custom Pairing Views**

If your app uses custom pairing views, you must ensure they work correctly in RTL layouts.

When a Right-to-Left (RTL) language is active, custom views may require adjustments to:

* layout direction
* text alignment
* margins, padding, or positioning
* icon placement and visual order

You can apply RTL-specific styling by using the CSS `:dir(rtl)` selector.

```css
.my-custom-class {
  transform: translate(-50%);
}

.my-custom-class:dir(rtl) {
  transform: translateX(50%);
}
```

For RTL-specific styling guidance, see [HTML and CSS Styling](/advanced/custom-views/html-and-css-styling#right-to-left-rtl-styling).

## Translating with OpenAI

To translate your app automatically, Homey CLI has a built-in integration that asks OpenAI to translate all `.json` and `README.txt` files. First, create an OpenAI API Key at <https://platform.openai.com/api-keys> and connect a payment method.

Then, execute in a command line:

```bash
$ homey app translate --api-key "..."
```

{% hint style="info" %}
We found that translating an entire app usually costs less than $1.
{% endhint %}


# Permissions

Homey Apps require permissions for certain functionality, learn which permissions exist and how to request them.

Permissions are needed to allow Homey apps to use certain Apps SDK functionality. Most Homey Apps don't need any permissions. In case a permission is not requested by the App, the manager methods that require that permission will throw an error.

Permissions are added to your [App Manifest](/the-basics/app/manifest) in an array named `permissions`.

{% code title="/.homeycompose/app.json" %}

```javascript
{
  "id": "com.athom.example",
  // ...
  "permissions": [
    "homey:wireless:ble",
    "homey:app:com.athom.example"
  ]
}
```

{% endcode %}

{% hint style="warning" %}
An app should only request permissions it actually needs to function.
{% endhint %}

{% hint style="warning" %}
Apps will not automatically update on Homey when new permissions are added.
{% endhint %}

## Available permissions

The following is a list of the permissions that an app may use.

| Permission                    | Description                                                                                                                                                         |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `homey:manager:api`           | Allows an app to use the [ManagerApi](https://apps-sdk-v3.developer.homey.app/ManagerApi.html) methods to communicate with the Homey Web API.                       |
| `homey:manager:geolocation`   | With this permission an app can use [ManagerGeolocation](https://apps-sdk-v3.developer.homey.app/ManagerGeolocation.html) to get the current location of the Homey. |
| `homey:manager:ledring`       | Allows interacting with the Homeys LED Ring through [ManagerLedring](https://apps-sdk-v3.developer.homey.app/ManagerLedring.html).                                  |
| `homey:manager:speech-output` | This permission allows an app to make Homey speak using [ManagerSpeechOutput](https://apps-sdk-v3.developer.homey.app/ManagerSpeechOutput.html).                    |
| `homey:wireless:433`          | With this permission an app can call `getSignal433` on [ManagerRF](https://apps-sdk-v3.developer.homey.app/ManagerRF.html) to use the 433MHz antenna.               |
| `homey:wireless:868`          | With this permission an app can call `getSignal868` on [ManagerRF](https://apps-sdk-v3.developer.homey.app/ManagerRF.html) to use the 868MHz antenna.               |
| `homey:wireless:ir`           | With this permission an app can call `getSignalInfrared` on [ManagerRF](https://apps-sdk-v3.developer.homey.app/ManagerRF.html) to send IR signals.                 |
| `homey:wireless:ble`          | Enables an app to use [ManagerBLE](https://apps-sdk-v3.developer.homey.app/ManagerBLE.html) to discover Bluetooth devices and interact with them.                   |
| `homey:wireless:nfc`          | Allows an app to use [ManagerNFC](https://apps-sdk-v3.developer.homey.app/ManagerNFC.html) to be notified of scanned NFC tags                                       |

## Which apps may use the API permission?

The API permission (`"homey:manager:api"`) allows an app to access the Homey Web API. This web api can be used to control all of Homey (devices, Flows, etc) even if they are not part of the App that requested the permission. This level of access can be used to add new advanced functionality to Homey. However because an app receives complete control over the Homey it is installed on this permission should only be requested when it is the main functionality of your app.

{% hint style="warning" %}
Apps that request the API permission will be reviewed more carefully when published to the App Store. If the permission is not required for your app to function your app submission will be rejected.
{% endhint %}

As a rule of thumb, only apps that add functionality to Homey that can be categorised in the Tools section, should use the API permission. For example apps that are allowed to use the API permission are: a DIY Home Alarm system, HomeyScript and Device Groups. Examples of apps that should not use the API permission are: those that connect to a physical device, e.g. a branded app for lightbulbs, thermostats etc.

{% hint style="info" %}
Apps using the `homey:manager:api` permission are not allowed on Homey Cloud. Read more about this in the [Homey Cloud guide](/guides/homey-cloud).
{% endhint %}

## App-to-app communication

Apps can also talk to each other through their [Web API](/advanced/web-api), however you need to define the correct permissions first. Permissions for app to app communication look like this `homey:app:<appId>`, where the `appId` is that of the app you want to communicate with. For example `homey:app:com.athom.example` or `homey:app:com.yahoo.weather`.

{% hint style="info" %}
App-to-app communication is not supported on Homey Cloud. Read more about this in the [Homey Cloud guide](/guides/homey-cloud).
{% endhint %}

For more information about app-to-app communication read the [Web API guide](/advanced/web-api#app-to-app-communication).


# Persistent Storage

There are various ways to store persistent data within your app.

Your app can store a user's settings in various ways. For each usecase, there's a best practice.

## Device Settings

Most likely, you'll want to store settings per-device.

For front-end, user-visible settings, use [Device Settings](/the-basics/devices/settings).

For back-end, persistent settings, use the Device Store. Refer to [Device.getStoreValue](https://apps-sdk-v3.developer.homey.app/Device.html#getStoreValue) and [Device.setStoreValue](https://apps-sdk-v3.developer.homey.app/Device.html#setStoreValue) for the documentation.

## App Settings

Most app-specific settings can be stored and retrieved through [ManagerSettings](https://apps-sdk-v3.developer.homey.app/ManagerSettings.html). This enabled your app to save and retrieve any value that is JSON-serializable. App Settings are saved across app restarts, and are only deleted when your app is uninstalled.

On Homey Pro, a custom App Settings can be created using HTML, CSS and JavaScript.

{% hint style="info" %}
It is generally discouraged to use custom HTML, because often app settings are better to be stored within a Device Store.
{% endhint %}

## App Userdata

For non-JSON serializable data, e.g. binary files, the `/userdata/` folder is writable on Homey Pro.

{% hint style="danger" %}
The `/userdata/` is publicly available on `http`s`://<homey>/app/your.app.id/userdata/`.

This allows for some nice usecases, but can also be a security risk! Be sure to keep your filenames unique so they cannot be guessed.

For example, when storing an image, don't name it `image1.jpg` but `a656d380-c887-4d8b-9ee5-f89de7b65d01.jpg` and keep the Image's name stored in the App Settings.
{% endhint %}


# Drivers & Devices

In Homey Apps the Device classes represent the physical devices paired with Homey.

{% embed url="<https://www.youtube.com/watch?v=1M1aNHqFBdc>" %}

For every device paired with Homey, a `Device` class will be created and a device tile will be shown in the interface. Every `Device` class is associated with a `Driver` class. All `Driver` classes of your app will be instantiated when your app is started, even if there are no devices of that type paired with Homey. This allows the driver to be responsible for pairing new devices and defining Flow cards.

![](/files/-MYoyCRJoV5nUeY2w59D)

You can use the Homey CLI to interactively create a driver, this will create a basic driver manifest and all the required files for your driver:

```bash
homey app driver create
```

This command will ask a number of questions about the driver you want to create, and will then create a new folder in your `drivers/` directory which will look like this:

{% tabs %}
{% tab title="JavaScript" %}

```
com.athom.example/driver/<driver_id>/
├─ assets/
│ └─ ...
├─ device.js
├─ driver.js
└─ driver.compose.json
```

The `/drivers/<driver_id>/driver.js` file contains the `Driver` class. This class is responsible for pairing devices and describes the functionality for the devices belonging to the driver such as Flow cards.

{% code title="/drivers/\<driver\_id>/driver.js" %}

```javascript
const Homey = require("homey");

class Driver extends Homey.Driver {
  // this method is called when the app is started and the Driver is inited
  async onInit() {
    const showToastActionCard = this.homey.flow.getActionCard('show_toast');
  
    showToastActionCard.registerRunListener(async ({ device, message }) => {
      await device.createToast(message);
    });
  }
  
  // This method is called when a user is adding a device
  // and the 'list_devices' view is called
  async onPairListDevices() {
    return [
      {
        name: "Foo Device",
        data: {
          id: "abcd1234",
        },
      },
    ];
  }
}

module.exports = Driver;
```

{% endcode %}

{% hint style="info" %}
The shown `onPairListDevices` is a heavily simplified example. Learn more about pairing in the [Pairing documentation](/the-basics/devices/pairing).
{% endhint %}

The `/drivers/<driver_id>/device.js` file contains the `Device` class. This class implements the device's functionality such as its capabilities and Flows.

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require("homey");
const DeviceApi = require("device-api");

class Device extends Homey.Device {
  // this method is called when the Device is inited
  async onInit() {
    this.log("Device init");
    this.log("Name:", this.getName());
    this.log("Class:", this.getClass());

    // register a capability listener
    this.registerCapabilityListener("onoff", this.onCapabilityOnoff.bind(this));
  }

  // this method is called when the Device has requested a state change (turned on or off)
  async onCapabilityOnoff(value, opts) {
    // ... set value to real device, e.g.
    // await setMyDeviceState({ on: value });
    // or, throw an error
    // throw new Error('Switching the device failed!');
  }
  
  // this is a custom method for the 'show_toast' Action Flow card as
  // shown in the Driver example above
  async createToast(message) {
    await DeviceApi.createToast(message);
  }
}

module.exports = Device;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}

```
com.athom.example/driver/<driver_id>/
├─ assets/
│ └─ ...
├─ device.mts
├─ driver.mts
└─ driver.compose.json
```

The `/drivers/<driver_id>/driver.mts` file contains the `Driver` class. This class is responsible for pairing devices and describes the functionality for the devices belonging to the driver such as Flow cards.

{% code title="/drivers/\<driver\_id>/driver.mts" %}

```mts
import Homey from 'homey';
import type Device from './device.mjs';

export default class Driver extends Homey.Driver {
  // this method is called when the app is started and the Driver is initialized
  async onInit(): Promise<void> {
    const showToastActionCard = this.homey.flow.getActionCard('show_toast');

    showToastActionCard.registerRunListener(async ({ device, message }: { device: Device; message: string }) => {
      await device.createToast(message);
    });
  }

  // This method is called when a user is adding a device
  // and the 'list_devices' view is called
  async onPairListDevices(): Promise<object[]> {
    return [
      {
        name: 'Foo Device',
        data: {
          id: 'abcd1234',
        },
      },
    ];
  }
}

```

{% endcode %}

{% hint style="info" %}
The shown `onPairListDevices` is a heavily simplified example. Learn more about pairing in the [Pairing documentation](/the-basics/devices/pairing).
{% endhint %}

The `/drivers/<driver_id>/device.mts` file contains the `Device` class. This class implements the device's functionality such as its capabilities and Flows.

{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey from 'homey';
import DeviceApi from 'device-api';

export default class Device extends Homey.Device {
  // this method is called when the Device is initialized
  async onInit(): Promise<void> {
    this.log('Device init');
    this.log('Name:', this.getName());
    this.log('Class:', this.getClass());

    // register a capability listener
    this.registerCapabilityListener('onoff', this.onCapabilityOnoff.bind(this));
  }

  // this method is called when the Device has requested a state change (turned on or off)
  async onCapabilityOnoff(value: boolean, opts: Record<string, unknown>): Promise<void> {
    // ... set value to real device, e.g.
    // await setMyDeviceState({ on: value });
    // or, throw an error
    // throw new Error('Switching the device failed!');
  }

  // this is a custom method for the 'show_toast' Action Flow card as
  // shown in the Driver example above
  async createToast(message: string): Promise<void> {
    await DeviceApi.createToast(message);
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}

```
com.athom.example/driver/<driver_id>/
├─ assets/
│ └─ ...
├─ device.py
├─ driver.py
└─ driver.compose.json
```

The `/drivers/<driver_id>/driver.py` file contains the `Driver` class. This class is responsible for pairing devices and describes the functionality for the devices belonging to the driver such as Flow cards.

{% code title="/drivers/\<driver\_id>/driver.py" %}

```python
from homey import driver
from homey.driver import ListDeviceProperties

from .device import Device


class Driver(driver.Driver):
    async def on_init(self) -> None:
        show_toast_action_card = self.homey.flow.get_action_card("show_toast")

        async def on_show_toast(card_arguments, **trigger_kwargs) -> None:
            device: Device = card_arguments["device"]
            message: str = card_arguments["message"]
            await device.create_toast(message)

        show_toast_action_card.register_run_listener(on_show_toast)

    # This method is called when a user is adding a device
    # and the 'list_devices' view is called
    async def on_pair_list_devices(self, view_data: dict) -> list[ListDeviceProperties]:
        return [{"name": "Foo Device", "data": {"id": "abcd1234"}}]


homey_export = Driver

```

{% endcode %}

{% hint style="info" %}
The shown `on_pair_list_devices` is a heavily simplified example. Learn more about pairing in the [Pairing documentation](/the-basics/devices/pairing).
{% endhint %}

The `/drivers/<driver_id>/device.py` file contains the `Device` class. This class implements the device's functionality such as its capabilities and Flows.

{% code title="/drivers/\<driver\_id>/device.py" %}

```python
from device_api import DeviceApi
from homey import device


class Device(device.Device):
    # this method is called when the Device is initialized
    async def on_init(self) -> None:
        self.log("Device init")
        self.log("Name:", self.get_name())
        self.log("Class:", self.get_class())

        # register a capability listener
        self.register_capability_listener("onoff", self.on_capability_onoff)

    async def on_capability_onoff(self, value: bool, **opts) -> None:
        # ... set the value to a real device, e.g.
        # await set_my_device_state(on=value)
        # or, throw an error
        # raise Exception("Switching the device failed!")
        pass

    # this is a custom method for the 'show_toast' Action Flow card as
    # shown in the Driver example above
    async def create_toast(self, message: str) -> None:
        await DeviceApi.create_toast(message)


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

## Driver Manifest

The `/drivers/<driver_id>/driver.compose.json` file is the driver manifest, when building your app all the `driver.compose.json` files will be bundled into your App Manifest. A basic driver manifest looks like this:

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "class": "socket",
  "capabilities": ["onoff", "dim"],
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png",
    "xlarge": "/drivers/my_driver/assets/images/xlarge.png"
  },
  "platforms": ["local", "cloud"],
  "connectivity": ["lan"],
  "pair": [
    {
      "id": "list_devices",
      "template": "list_devices",
      "navigation": { "next": "add_devices" }
    },
    {
      "id": "add_devices",
      "template": "add_devices"
    }
  ]
}
```

{% endcode %}

### Icon

The driver icon location cannot be specified in the driver manifest, instead the driver's icon is always expected to be located at `/drivers/<driver_id>/assets/icon.svg`. Read the [app store guidelines](https://apps.developer.homey.app/the-basics/pages/-MWOPi9AQ4_R9f5UnnjY#1.5.-icons) for more information about the driver icon.

### Platforms

`"platforms": ["local", "cloud"]`

An array containing the driver's supported platforms. Read the [Homey Cloud](/guides/homey-cloud) guide for more information.

### Device Class

`"class": "light"`

The device class tells Homey what type of device your driver adds support for. Examples of device classes are `socket`, `light`, `lock` etc. When a specific device is not supported by Homey, you can use the class `other`.

Device classes play a crucial role in enhancing the user experience within the Homey platform. By properly configuring classes for your devices, you ensure seamless integration with various features and services.

For instance, if a user utilizes a Zone Flow card to turn off all lights in a specific zone, your device will be automatically included if it has both the `onoff` capability and the `light` class. This ensures that the device is correctly recognized and controlled as a light.

Additionally, when a user has linked their Homey setup to Google Assistant, commands like "Turn off all lights" will function as expected. This is because you've assigned the appropriate class to your device, allowing it to be correctly identified and managed by voice assistants.

By carefully setting device classes, you help create a smooth and intuitive experience for users, both within the Homey ecosystem and with integrated third-party services like Google Assistant.

Find your device's class in the [Device Class Reference](https://apps-sdk-v3.developer.homey.app/tutorial-device-classes.html).

### Capabilities

`"capabilities": ["onoff", "dim"]`

The capabilities of a device describe the states and actions a device supports. For example, a light may have the capability `onoff` which allows users to toggle the light on or off. It can also support the `dim` capability which would allow the user to change the lights brightness.

Capabilities all have a data type, for example the `onoff` capability has the type `boolean` and can thus be either `true` or `false`. The capability `dim` is of type `number` and can be any number between `0 - 1`, as defined in the capability's definition.

Homey ships with many system capabilities. For other cases, app-specific capabilities can be defined in the [App Manifest](/the-basics/app/manifest).

Homey has built-in [Flow](/the-basics/flow) cards for every system capability.

Read more about [Capabilities](/the-basics/devices/capabilities).

### Energy

`"energy": {...}`

A device can use or generate power. To keep track of the data related to power usage and generation a device can have an energy object. This object contains power usage approximation data and flags to indicate a device generates power or should be omitted from automatic shutdown.

Read more about [Energy](/the-basics/devices/energy).

### Settings

`"settings": [ ... ]`

A device can have user-configurable settings, such as the orientation of a curtain or a poll-interval. Settings are shown to the user in the front-end, or can be changed programmatically (see [`Device#setSettings`](https://apps-sdk-v3.developer.homey.app/Device.html#setSettings)).

Read more about [Settings](/the-basics/devices/settings).

### Pairing

`"pair": [ ... ]`

A device can be added to Homey through pairing. Pairing is started when the user selects the device they want to add from the Homey app. The `pair` property of the device manifest describes the steps necessary to add the device to Homey.

For most devices a few simple pair steps are enough. For more advanced devices, where more user-steps are required, custom pairing views can be provided.

Read more about [Pairing](/the-basics/devices/pairing).

### Deprecated

`"deprecated": true`

Sometimes a Driver that has been available in the past should be removed. To not break compatibility for users that were using it, add `"deprecated": true` to your driver in your App Manifest. It will still work, but won't show up anymore in the 'Add Device' list.

### Connectivity

`"connectivity": [ ... ]`

Specify how the Driver connects to your device in the real world. You can specify multiple values, for example `[ "infrared", "lan" ]` for a TV that is turned on by Infrared, and then controlled over Wi-Fi LAN.

#### Allowed Values

| Value      | Description                                       |
| ---------- | ------------------------------------------------- |
| `lan`      | Local (Wi-Fi/Ethernet)                            |
| `cloud`    | Cloud-connected (Wi-Fi/Ethernet)                  |
| `ble`      | Bluetooth Low Energy                              |
| `zwave`    | Z-Wave                                            |
| `zigbee`   | Zigbee                                            |
| `infrared` | Infrared                                          |
| `rf433`    | 433 MHz                                           |
| `rf868`    | 868 MHz                                           |
| `matter`   | Matter (Only available on Homey Pro (Early 2023)) |

## Device identifier

During pairing, you must provide a `data` property. This property contains a unique identifier for the device. This object cannot be changed after pairing. This `data` property is an object containing any properties of the types String, Number or Object. Homey uses this object to identify your device, together with the driver's ID. Read more in the [Device pairing documentation](/the-basics/devices/pairing).

{% hint style="warning" %}
Only put the essential properties needed to identify a device in the data object. For example, a MAC address is a good property, an IP address is not, because it can change over time.
{% endhint %}

Any properties that could change over time should be kept in-memory or saved in the device's store.

## Availability

{% tabs %}
{% tab title="JavaScript" %}
A device can be marked as unavailable using [`Device#setUnavailable()`](https://apps-sdk-v3.developer.homey.app/Device.html#setUnavailable). This shows to the user that they cannot interacting with the device for example because the device is offline.

When a device is marked as unavailable, all capabilities and Flow actions will be prevented. When a device is available again, use [`Device#setAvailable()`](https://apps-sdk-v3.developer.homey.app/Device.html#setAvailable) to mark the device as available.

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require("homey");
const DeviceApi = require("device-api");

class Device extends Homey.Device {
  async onInit() {
    await this.setUnavailable();

    DeviceApi.on("connected", (address) => {
      this.setAvailable().catch(this.error);
    });
    
    DeviceApi.on("disconnected", (address) => {
      this.setUnavailable().catch(this.error);
    });    
  }
}

module.exports = Device;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
A device can be marked as unavailable using [`Device#setUnavailable()`](https://apps-sdk-v3.developer.homey.app/Device.html#setUnavailable). This shows to the user that they cannot interacting with the device for example because the device is offline.

When a device is marked as unavailable, all capabilities and Flow actions will be prevented. When a device is available again, use [`Device#setAvailable()`](https://apps-sdk-v3.developer.homey.app/Device.html#setAvailable) to mark the device as available.

{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey from 'homey';
import DeviceApi from 'device-api';

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    await this.setUnavailable();

    DeviceApi.on('connected', (address: string) => {
      this.setAvailable().catch(this.error);
    });

    DeviceApi.on('disconnected', (address: string) => {
      this.setUnavailable().catch(this.error);
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
A device can be marked as unavailable using [`Device#set_unavailable()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.set_unavailable). This shows to the user that they cannot interacting with the device for example because the device is offline.

When a device is marked as unavailable, all capabilities and Flow actions will be prevented. When a device is available again, use [`Device#set_available()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.set_available) to mark the device as available.

{% code title="/drivers/\<driver\_id>/device.py" %}

```python
from device_api import DeviceApi
from homey import device


class Device(device.Device):
    async def on_init(self) -> None:
        await self.set_unavailable()

        async def on_connected(address: str) -> None:
            try:
                await self.set_available()
            except Exception as e:
                self.error(e)

        async def on_disconnected(address: str) -> None:
            try:
                await self.set_unavailable()
            except Exception as e:
                self.error(e)

        DeviceApi.on("connected", on_connected)
        DeviceApi.on("disconnected", on_disconnected)


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

## Assets

Your drivers `/assets/` folder contains the icon and images for that driver. These images should be clean marketing pictures of the device that this driver implements, these are shown in the Homey App Store.

Read more about driver images and icons in the [Homey App Store guidelines](/app-store/guidelines#1-4-images).

## Store

In some cases you may want to store some information about the device that persists across reboots. For example you may need to store the device's IP address. The device's store is a persistent storage to save device properties. This store can be set during pairing and programmatically read and updated after the device is paired with Homey.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require("homey");
const DeviceApi = require("device-api");

class Device extends Homey.Device {
  async onInit() {
    this.currentAddress = this.getStoreValue("address");

    DeviceApi.on("address-changed", (address) => {
      this.currentAddress = address;
      this.setStoreValue("address", address).catch(this.error);
    });
  }
}

module.exports = Device;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey from 'homey';
import DeviceApi from 'device-api';

export default class Device extends Homey.Device {
  currentAddress?: string;

  async onInit(): Promise<void> {
    this.currentAddress = this.getStoreValue('address');

    DeviceApi.on('address-changed', (address: string) => {
      this.currentAddress = address;
      this.setStoreValue('address', address).catch(this.error);
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/drivers/\<driver\_id>/device.py" %}

```python
from device_api import DeviceApi
from homey import device


class Device(device.Device):
    current_address: str | None

    async def on_init(self) -> None:
        self.current_address = self.get_store().get("address")

        async def on_address_changed(address: str) -> None:
            self.current_address = address
            try:
                await self.set_store_value("address", address)
            except Exception as e:
                self.error(e)

        DeviceApi.on("address-changed", on_address_changed)


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

{% hint style="info" %}
Using the store is pretty rare, usually there are other solutions.\
For example if you want users to be able to update these values easily you should use device settings instead. See the [device settings documentation](/the-basics/devices/settings) for more information.\
And instead of storing the devices IP address in the device store you could use the [local network device discovery](/wireless/wi-fi/discovery) functionality that is built into Homey.
{% endhint %}


# Pairing

Pairing allows users to add new devices to Homey.

![](/files/-MZwDxE13t2NnFrZn2Yg)

Pairing is started when the user selects the device they want to add from the Homey app. The `pair` property of the driver defines a list of views, which the user navigates through. These views are called pairing templates. Homey includes a set of system templates that implement consistent pairing steps for most devices.

{% hint style="info" %}
Homey already knows how to pair Zigbee and Z-Wave devices so it is not possible to implement your own pairing for those devices. Read the [Zigbee](/wireless/zigbee) and [Z-Wave](/wireless/z-wave) documentations to learn how to pair devices using those technologies with Homey.
{% endhint %}

## Basic pairing example

This example is a basic way to enable pairing in your driver. To add pairing to your driver, add the following to your `driver.compose.json`:

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "class": "socket",
  "capabilities": ["onoff"],
  "platforms": ["local", "cloud"],
  "connectivity": "cloud",
  "pair": [
    {
      "id": "list_my_devices",
      // we use a system template here, for consistency, and less work for us!
      "template": "list_devices",
      // show pair view with id 'add_my_devices' when clicked 'Next'
      "navigation": { "next": "add_my_devices" }
    },
    {
      "id": "add_my_devices",
      // again, use a template
      "template": "add_devices"
    }
  ]
}
```

{% endcode %}

This defines a pairing process with 2 steps. The first step requires the user to pick the devices to add from a list and the second step will automatically add these devices to Homey. Both these steps use Homey's built-in pairing templates `list_devices` and `add_devices`. The navigation option determines which of the steps the pairing will go to when the user presses the "Next" button.

{% hint style="info" %}
The `navigation` object also supports a `prev` option for when a user can go back to the previous screen. This can be useful, for example, with a `login_credentials` system templates to allow users to retry logging-in.
{% endhint %}

{% tabs %}
{% tab title="JavaScript" %}
If your pairing only uses the `list_devices` and `add_devices` templates you can use the [`Driver#onPairListDevices()`](https://apps-sdk-v3.developer.homey.app/Driver.html#onPairListDevices) method to quickly implement a pairing process. From this method you can return a list of devices that will be presented to the user.

{% code title="/drivers/\<driver\_id>/driver.js" %}

```javascript
const Homey = require("homey");
const DeviceApi = require("device-api");

class Driver extends Homey.Driver {
  async onPairListDevices() {
    const devices = await DeviceApi.discoverDevices();
    return devices;
  }
}

module.exports = Driver;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
If your pairing only uses the `list_devices` and `add_devices` templates you can use the [`Driver#onPairListDevices()`](https://apps-sdk-v3.developer.homey.app/Driver.html#onPairListDevices) method to quickly implement a pairing process. From this method you can return a list of devices that will be presented to the user.

{% code title="/drivers/\<driver\_id>/driver.mts" %}

```mts
import Homey from "homey";
import DeviceApi from "./device-api.mjs";

export default class Driver extends Homey.Driver {
  async onPairListDevices(): Promise<object[]> {
    const devices = await DeviceApi.discoverDevices();
    return devices;
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
If your pairing only uses the `list_devices` and `add_devices` templates you can use the [`Driver#on_pair_list_devices()`](https://python-apps-sdk-v3.developer.homey.app/driver.html#homey.driver.Driver.on_pair_list_devices) method to quickly implement a pairing process. From this method you can return a list of devices that will be presented to the user.

{% code title="/drivers/\<driver\_id>/driver.py" %}

```python
from device_api import DeviceApi
from homey import driver
from homey.driver import ListDeviceProperties


class Driver(driver.Driver):
    async def on_pair_list_devices(self, view_data: dict) -> list[ListDeviceProperties]:
        devices = await DeviceApi.discover_devices()
        return devices


homey_export = Driver

```

{% endcode %}
{% endtab %}
{% endtabs %}

## Device pairing data

{% tabs %}
{% tab title="JavaScript" %}
The following is an overview of the data you can supply for a device to be added, you should return an array of objects like this from [`Driver#onPairListDevices()`](https://apps-sdk-v3.developer.homey.app/Driver.html#onPairListDevices) or `session.setHandler("list_devices")`.
{% endtab %}

{% tab title="TypeScript" %}
The following is an overview of the data you can supply for a device to be added, you should return an array of objects like this from [`Driver#onPairListDevices()`](https://apps-sdk-v3.developer.homey.app/Driver.html#onPairListDevices) or `session.setHandler("list_devices")`.
{% endtab %}

{% tab title="Python" %}
The following is an overview of the data you can supply for a device to be added, you should return an array of objects like this from [`Driver#on_pair_list_devices()`](https://python-apps-sdk-v3.developer.homey.app/driver.html#homey.driver.Driver.on_pair_list_devices) or `session.set_handler("list_devices")`.
{% endtab %}
{% endtabs %}

```javascript
{
  // The name of the device that will be displayed
  name: "My Device",

  // The data object is required and should be unique for the device.
  // So a device's MAC address would be good, but an IP address would
  // be bad since it can change over time.
  data: {
    id: "abcd",
  },

  // Optional: The store is dynamic and persistent storage for your device
  store: {
    // For example store the IP address of your device
    address: "127.0.0.1",
  },

  // Optional: sets the devices initial settings, this allows users to change
  // them after pairing in the device settings screen.
  settings: {
    pincode: "1234",
  },

  // Optional: These properties overwrite the defaults
  // that you specified in the driver manifest:
  icon: "/my_icon.svg", // relative to: /drivers/<driver_id>/assets/
  capabilities: ["onoff", "target_temperature"],
  capabilitiesOptions: {
    target_temperature: {
      min: 5,
      max: 35,
    },
  },
}
```

{% hint style="info" %}
Note that it is also supported that icons are referenced from the `/userdata` folder. This is the only exception to the rule that all icons are relative to: `/drivers/<driver_id>/assets/` e.g. `/userdata/my_icon.svg`.\
\
This allows apps to upload icons to the userdata folder and reference them later during pairing. This feature is supported since Homey `v12.3.0`
{% endhint %}

## System Views

The pairing templates are built with HTML, CSS and JavaScript. Most drivers will suffice using the system templates that are provided by Homey.

{% content-ref url="/pages/Md8Fb8YLMnUBDpSu7CAF" %}
[Devices List](/the-basics/devices/pairing/system-views/devices-list)
{% endcontent-ref %}

{% content-ref url="/pages/RBzbzHLKKRVpx53NcZxU" %}
[Add Devices](/the-basics/devices/pairing/system-views/add-devices)
{% endcontent-ref %}

{% content-ref url="/pages/tP5F2ODmStSNh5F04HZ7" %}
[OAuth2 Login](/the-basics/devices/pairing/system-views/oauth2-login)
{% endcontent-ref %}

{% content-ref url="/pages/o7dnMnPfKGaPdWhkWdqT" %}
[Credentials Login](/the-basics/devices/pairing/system-views/credentials-login)
{% endcontent-ref %}

{% content-ref url="/pages/nke6pGcRPtGy2kspPHrg" %}
[Pincode](/the-basics/devices/pairing/system-views/pincode)
{% endcontent-ref %}

{% content-ref url="/pages/iTR43AkzIUDozivYFmSx" %}
[Loading](/the-basics/devices/pairing/system-views/loading)
{% endcontent-ref %}

{% content-ref url="/pages/DXZXCGFExdlqHY8nnCOn" %}
[Done](/the-basics/devices/pairing/system-views/done)
{% endcontent-ref %}

## Custom Views

Most drivers will suffice using the provided templates. In certain cases you may want, or need, to create pairing screens that are more suited to your driver. For these cases it is possible to create custom pairing views, to learn more read the [custom pairing view guide](/advanced/custom-views/custom-pairing-views).

## Repairing

To ensure users with a great experience, your app's devices should always stay available without user interaction.

However, sometimes when a device explicitly needs user interaction to be fixed (for example an OAuth2 token has been revoked and the user needs to authenticate again), the user can initiate a *repair* process.

To enable repairing, you must add support for this to your driver by adding `repair` to your App Manifest:

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "repair": [
    {
      "id": "login_oauth2",
      "template": "login_oauth2"
    }
  ]
}
```

{% endcode %}

It is also possible to use custom pairing views for repairing. To learn more about custom pairing templates read the [custom pairing view guide](/advanced/custom-views/custom-pairing-views).

{% tabs %}
{% tab title="JavaScript" %}
Note that when repairing the `Homey.createDevice()` method is not available in the custom view and you can add a `onRepair` method to your driver, which is similar to the `onPair` method.

{% code title="/drivers/\<driver\_id>/driver.js" %}

```javascript
const Homey = require("homey");

class Driver extends Homey.Driver {
  onRepair(session, device) {
    // Argument session is a PairSocket, similar to Driver.onPair
    // Argument device is a Homey.Device that's being repaired

    session.setHandler("my_event", (data) => {
      // Your code
    });

    session.setHandler("disconnect", () => {
      // Cleanup
    });
  }
}

module.exports = Driver;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
Note that when repairing the `Homey.createDevice()` method is not available in the custom view and you can add a `onRepair` method to your driver, which is similar to the `onPair` method.

{% code title="/drivers/\<driver\_id>/driver.mts" %}

```mts
import Homey from "homey";
import type Device from "./device.mjs";

export default class Driver extends Homey.Driver {
  async onRepair(session: Homey.Driver.PairSession, device: Device): Promise<void> {
    // Argument device is the device that's being repaired

    session.setHandler("my_event", async data => {
      // Your code
    });

    session.setHandler("disconnect", async () => {
      // Cleanup
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
Note that when repairing the `Homey.createDevice()` method is not available in the custom view and you can add a `on_repair` method to your driver, which is similar to the `on_pair` method.

{% code title="/drivers/\<driver\_id>/driver.py" %}

```python
from homey import driver
from homey.pair_session import PairSession

from .device import Device


class Driver(driver.Driver[Device]):
    async def on_repair(self, session: PairSession, device: Device):
        # Argument device is the device that's being repaired

        async def on_my_event(*args):
            # Your code
            ...

        async def on_disconnect():
            # Cleanup
            ...

        session.set_handler("my_event", on_my_event)
        session.set_handler("disconnect", on_disconnect)


homey_export = Driver

```

{% endcode %}
{% endtab %}
{% endtabs %}


# System Views

The pairing templates are built with HTML, CSS and JavaScript. Most drivers will suffice using the system templates that are provided by Homey.

## Templates

{% content-ref url="/pages/Md8Fb8YLMnUBDpSu7CAF" %}
[Devices List](/the-basics/devices/pairing/system-views/devices-list)
{% endcontent-ref %}

{% content-ref url="/pages/RBzbzHLKKRVpx53NcZxU" %}
[Add Devices](/the-basics/devices/pairing/system-views/add-devices)
{% endcontent-ref %}

{% content-ref url="/pages/tP5F2ODmStSNh5F04HZ7" %}
[OAuth2 Login](/the-basics/devices/pairing/system-views/oauth2-login)
{% endcontent-ref %}

{% content-ref url="/pages/o7dnMnPfKGaPdWhkWdqT" %}
[Credentials Login](/the-basics/devices/pairing/system-views/credentials-login)
{% endcontent-ref %}

{% content-ref url="/pages/nke6pGcRPtGy2kspPHrg" %}
[Pincode](/the-basics/devices/pairing/system-views/pincode)
{% endcontent-ref %}

{% content-ref url="/pages/iTR43AkzIUDozivYFmSx" %}
[Loading](/the-basics/devices/pairing/system-views/loading)
{% endcontent-ref %}

{% content-ref url="/pages/DXZXCGFExdlqHY8nnCOn" %}
[Done](/the-basics/devices/pairing/system-views/done)
{% endcontent-ref %}


# Devices List

This view will show a list of selectable devices to the user. Devices that have already been paired with Homey will automatically be filtered out, based on their data property.

**Usage:** `"template": "list_devices"`

<figure><img src="/files/HaPu8wXkPeBge99f1Xsl" alt=""><figcaption></figcaption></figure>

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "pair": [
    {
      "id": "list_devices",
      "template": "list_devices",
      "navigation": { "next": "add_devices" },
      "options": { "singular": true }
    },
    {
      "id": "add_devices",
      "template": "add_devices"
    }
  ]
}
```

{% endcode %}

## **Options**

| Key        | Type      | Default | Description                               |
| ---------- | --------- | ------- | ----------------------------------------- |
| `singular` | `boolean` | `false` | Only allow a single device to be selected |

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/drivers/\<driver\_id>/driver.js" %}

```javascript
const Homey = require("homey");
const DeviceApi = require("device-api");

class Driver extends Homey.Driver {
  async onPair(session) {
    session.setHandler("list_devices", async function () {
      const devices = await DeviceApi.discoverDevices();
      
      // you can emit when devices are still being searched
      // session.emit("list_devices", devices);

      // return devices when searching is done
      return devices;

      // when no devices are found, return an empty array
      // return [];

      // or throw an Error to show that instead
      // throw new Error('Something bad has occured!');
    });
  }
}

module.exports = Driver;
```

{% endcode %}

Because the `list_devices` template is very common the [`Driver#onPairListDevices()`](https://apps-sdk-v3.developer.homey.app/Driver.html#onPairListDevices) method exists which you can implement instead of the [`Driver#onPair()`](https://apps-sdk-v3.developer.homey.app/Driver.html#onPair) method.
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/drivers/\<driver\_id>/driver.mts" %}

```mts
import Homey from "homey";
import DeviceApi from "./device-api.mjs";

export default class Driver extends Homey.Driver {
  async onPair(session: Homey.Driver.PairSession): Promise<void> {
    session.setHandler("list_devices", async (): Promise<object[]> => {
      const devices = await DeviceApi.discoverDevices();

      // you can emit when devices are still being searched
      // session.emit("list_devices", devices);

      // return devices when searching is done
      return devices;

      // when no devices are found, return an empty array
      // return [];

      // or throw an Error to show that instead
      // throw new Error('Something bad has occurred!');
    });
  }
}

```

{% endcode %}

Because the `list_devices` template is very common the [`Driver#onPairListDevices()`](https://apps-sdk-v3.developer.homey.app/Driver.html#onPairListDevices) method exists which you can implement instead of the [`Driver#onPair()`](https://apps-sdk-v3.developer.homey.app/Driver.html#onPair) method.
{% endtab %}

{% tab title="Python" %}
{% code title="/drivers/\<driver\_id>/driver.py" %}

```python
from device_api import DeviceApi
from homey import driver
from homey.pair_session import PairSession


class Driver(driver.Driver):
    async def on_pair(self, session: PairSession) -> None:
        async def on_list_device():
            devices = await DeviceApi.discover_devices()

            # you can emit when devices are still being searched
            # session.emit("list_devices", devices);

            # return devices when searching is done
            return devices

            # when no devices are found, return an empty list
            # return []

            # or throw an Error to show that instead
            # raise Exception("Something bad has occurred!")

        session.set_handler("list_devices", on_list_device)


homey_export = Driver

```

{% endcode %}

Because the `list_devices` template is very common the [`Driver#on_pair_list_devices()`](https://python-apps-sdk-v3.developer.homey.app/driver.html#homey.driver.Driver.on_pair_list_devices) method exists which you can implement instead of the [`Driver#on_pair()`](https://python-apps-sdk-v3.developer.homey.app/driver.html#homey.driver.Driver.on_pair) method.
{% endtab %}
{% endtabs %}


# Add Devices

This view will simply add the devices as selected by list\_devices, and finish the pairing session.

**Usage:** `"template": "add_devices"`


# OAuth2 Login

This view can be used for devices that need OAuth2 authorization. When it's successful, it will automatically proceed to the next view.

**Usage:** `"template": "login_oauth2"`

{% hint style="warning" %}
The example below is for completeness only. Pleaser read [OAuth2](/cloud/oauth2) to learn how to integrate with OAuth2 APIs the easy way.
{% endhint %}

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "pair": [
    {
      "id": "login_oauth2",
      "template": "login_oauth2",
      "options": {
        "hint": "Login with your credentials",
        "button": "Log-in"
      }
    },
    {
      "id": "list_devices",
      "template": "list_devices",
      "navigation": { "next": "add_devices" }
    },
    {
      "id": "add_devices",
      "template": "add_devices"
    }
  ]
}
```

{% endcode %}

## **Options**

| Key        | Type                                                       | Default | Description |
| ---------- | ---------------------------------------------------------- | ------- | ----------- |
| `title`    | [translation object](/the-basics/app/internationalization) |         |             |
| `subtitle` | [translation object](/the-basics/app/internationalization) |         |             |
| `hint`     | [translation object](/the-basics/app/internationalization) | `""`    |             |
| `button`   | [translation object](/the-basics/app/internationalization) | `""`    |             |

When either `hint` or `button` are set to a value, a button will appear and wait for the user to click it before opening the popup.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/drivers/\<driver\_id>/driver.js" %}

```javascript
const Homey = require("homey");

const API_URL = "https://api.myservice.com/oauth2/authorise?response_type=code";
const CALLBACK_URL = "https://callback.athom.com/oauth2/callback/";
const CLIENT_ID = Homey.env.CLIENT_ID;
const OAUTH_URL = `${API_URL}&client_id=${CLIENT_ID}&redirect_uri=${CALLBACK_URL}`;

class Driver extends Homey.Driver {
  async onPair(session) {
    const myOAuth2Callback = await this.homey.cloud.createOAuth2Callback(OAUTH_URL);

    myOAuth2Callback
      .on("url", (url) => {
        // send the URL to the front-end to open a popup
        session.emit("url", url);
      })
      .on("code", (code) => {
        // ... swap your code here for an access token

        // tell the front-end we're done
        session.emit("authorized");
      });
  }
}

module.exports = Driver;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/drivers/\<driver\_id>/driver.js" %}

```mts
import Homey from "homey";

const API_URL = "https://api.myservice.com/oauth2/authorise?response_type=code";
const CALLBACK_URL = "https://callback.athom.com/oauth2/callback/";
const CLIENT_ID = Homey.env.CLIENT_ID;
const OAUTH_URL = `${API_URL}&client_id=${CLIENT_ID}&redirect_uri=${CALLBACK_URL}`;

export default class Driver extends Homey.Driver {
  async onPair(session: Homey.Driver.PairSession): Promise<void> {
    const myOauth2Callback = await this.homey.cloud.createOAuth2Callback(OAUTH_URL);

    myOauth2Callback
      .on("url", (url: string) => {
        // send the URL to the front-end to open a popup
        session.emit("url", url);
      })
      .on("code", (code: string) => {
        // ... swap your code here for an access token

        // tell the front-end we're done
        session.emit("authorized", undefined);
      });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/drivers/\<driver\_id>/driver.py" %}

```python
import asyncio

from homey import driver
from homey.homey import Homey
from homey.pair_session import PairSession

API_URL = "https://api.myservice.com/oauth2/authorise?response_type=code"
CALLBACK_URL = "https://callback.athom.com/oauth2/callback/"
CLIENT_ID = Homey.env.CLIENT_ID
OAUTH_URL = f"{API_URL}&client_id={CLIENT_ID}&redirect_uri={CALLBACK_URL}"


class Driver(driver.Driver):
    async def on_pair(self, session: PairSession) -> None:
        my_oauth2_callback = await self.homey.cloud.create_oauth2_callback(OAUTH_URL)

        def on_url(url: str):
            # send the URL to the front-end to open a popup
            asyncio.create_task(session.emit("url", url))

        def on_code(code: str | Exception):
            # ... swap your code here for an access token

            # tell the front-end we're done
            asyncio.create_task(session.emit("authorized"))

        my_oauth2_callback.on_url(on_url)
        my_oauth2_callback.on_code(on_code)


homey_export = Driver

```

{% endcode %}
{% endtab %}
{% endtabs %}


# Credentials Login

This pair template shows a username & password view where the user can enter credentials.

**Usage:** `"template": "login_credentials"`

<figure><img src="/files/4qk7jXfIOHXxPGreWLI1" alt=""><figcaption></figcaption></figure>

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "pair": [
    {
      "id": "login_credentials",
      "template": "login_credentials",
      "options": {
        "logo": "logo.png",
        "title": {
          "en": "Your custom title"
        },
        "usernameLabel": { "en": "E-mail address" },
        "usernamePlaceholder": { "en": "john@doe.com" },
        "passwordLabel": { "en": "Password" },
        "passwordPlaceholder": { "en": "Password" }
      }
    },
    {
      "id": "list_devices",
      "template": "list_devices",
      "navigation": { "next": "add_devices" }
    },
    {
      "id": "add_devices",
      "template": "add_devices"
    }
  ]
}
```

{% endcode %}

## **Options**

| Key                   | Type                                                       | Default            | Description                   |
| --------------------- | ---------------------------------------------------------- | ------------------ | ----------------------------- |
| `title`               | [translation object](/the-basics/app/internationalization) |                    |                               |
| `logo`                | `string`                                                   | `null`             | A path to an image for a logo |
| `usernameLabel`       | [translation object](/the-basics/app/internationalization) | `"E-mail address"` |                               |
| `usernamePlaceholder` | [translation object](/the-basics/app/internationalization) | `"john@doe.com"`   |                               |
| `passwordLabel`       | [translation object](/the-basics/app/internationalization) | `"Password"`       |                               |
| `passwordPlaceholder` | [translation object](/the-basics/app/internationalization) | `"Password"`       |                               |

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/drivers/\<driver\_id>/driver.js" %}

```javascript
const Homey = require("homey");
const DeviceAPI = require("device-api");

class Driver extends Homey.Driver {
  async onPair(session) {
    let username = "";
    let password = "";

    session.setHandler("login", async (data) => {
      username = data.username;
      password = data.password;

      const credentialsAreValid = await DeviceAPI.testCredentials({
        username,
        password,
      });

      // return true to continue adding the device if the login succeeded
      // return false to indicate to the user the login attempt failed
      // thrown errors will also be shown to the user
      return credentialsAreValid;
    });

    session.setHandler("list_devices", async () => {
      const api = await DeviceAPI.login({ username, password });
      const myDevices = await api.getDevices();

      const devices = myDevices.map((myDevice) => {
        return {
          name: myDevice.name,
          data: {
            id: myDevice.id,
          },
          settings: {
            // Store username & password in settings
            // so the user can change them later
            username,
            password,
          },
        };
      });

      return devices;
    });
  }
}

module.exports = Driver;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/drivers/\<driver\_id>/driver.mts" %}

```mts
import Homey from "homey";
import DeviceApi from "./device-api.mjs";

type LoginData = {
  username: string;
  password: string;
};

export default class Driver extends Homey.Driver {
  async onPair(session: Homey.Driver.PairSession): Promise<void> {
    let username = "";
    let password = "";

    session.setHandler("login", async (data: LoginData): Promise<boolean> => {
      username = data.username;
      password = data.password;

      const credentialsAreValid = await DeviceApi.testCredentials(username, password);

      // return true to continue adding the device if the login succeeded
      // return false to indicate to the user the login attempt failed
      // thrown errors will also be shown to the user
      return credentialsAreValid;
    });

    session.setHandler("list_devices", async (): Promise<object[]> => {
      const api = await DeviceApi.login(username, password);
      const myDevices = await api.getDevices();

      const devices = myDevices.map(myDevice => {
        return {
          name: myDevice.name,
          data: {
            id: myDevice.id,
          },
          settings: {
            // Store username & password in settings
            // so the user can change them later
            username: username,
            password: password,
          },
        };
      });

      return devices;
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/drivers/\<driver\_id>/driver.py" %}

```python
from typing import TypedDict

from device_api import DeviceApi
from homey import driver
from homey.driver import ListDeviceProperties
from homey.pair_session import PairSession


class LoginData(TypedDict):
    username: str
    password: str


class Driver(driver.Driver):
    async def on_pair(self, session: PairSession) -> None:
        username, password = "", ""

        async def on_login(data: LoginData) -> bool:
            nonlocal username, password
            username = data["username"]
            password = data["password"]

            credentials_are_valid = await DeviceApi.test_credentials(**data)

            # return true to continue adding the device if the login succeeded
            # return false to indicate to the user the login attempt failed
            # thrown errors will also be shown to the user
            return credentials_are_valid

        async def on_list_devices() -> list[ListDeviceProperties]:
            api = await DeviceApi.login(username, password)
            my_devices = await api.get_devices()

            devices: list[ListDeviceProperties] = [
                {
                    "name": device.name,
                    "data": {"id": device.id},
                    "settings": {
                        # Store username & password in settings
                        # so the user can change them later
                        "username": username,
                        "password": password,
                    },
                }
                for device in my_devices
            ]

            return devices


homey_export = Driver

```

{% endcode %}
{% endtab %}
{% endtabs %}


# Pincode

This pair template shows a pincode input. When the pincode is correct, it will proceed to the next view.

**Usage:** `"template": "pincode"`

<figure><img src="/files/uyQ6uQkfzcgThf33chf2" alt=""><figcaption></figcaption></figure>

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "pair": [
    {
      "id": "list_devices",
      "template": "list_devices",
      "navigation": { "next": "pincode" }
    },
    {
      "id": "pincode",
      "template": "pincode",
      "options": {
        "title": "Login with your account",
        "hint": "Enter the device's pincode",
        "type": "number",
        "length": 4
      }
    },
    {
      "id": "add_devices",
      "template": "add_devices"
    }
  ]
}
```

{% endcode %}

## **Options**

| Key      | Type                                                       | Default            | Description                                                                                                                |
| -------- | ---------------------------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| `type`   | `string`                                                   | `"number"`         | Either `number` or `text`. This changes how the keyboard is presented on mobile phones when the pincode field is selected. |
| `length` | `number`                                                   | `4`                | The number of characters                                                                                                   |
| `hint`   | [translation object](/the-basics/app/internationalization) | `""`               |                                                                                                                            |
| `title`  | [translation object](/the-basics/app/internationalization) | `"Enter pincode:"` |                                                                                                                            |

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/drivers/\<driver\_id>/driver.js" %}

```javascript
const Homey = require("homey");

class Driver extends Homey.Driver {
  onPair(session) {
    session.setHandler("pincode", async (pincode) => {
      // The pincode is given as an array of the filled in values
      return (
        pincode[0] === "1"
        && pincode[1] === "2" 
        && pincode[2] === "3"
        && pincode[3] === "4"
      );
    });
  }
}

module.exports = Driver;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/drivers/\<driver\_id>/driver.mts" %}

```mts
import Homey from "homey";

export default class Driver extends Homey.Driver {
  async onPair(session: Homey.Driver.PairSession): Promise<void> {
    session.setHandler("pincode", async (pincode: string[]): Promise<boolean> => {
      // The pincode is given as an array of the filled in values
      return (
        pincode[0] === "1"
        && pincode[1] === "2"
        && pincode[2] === "3"
        && pincode[3] === "4"
      );
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/drivers/\<driver\_id>/driver.py" %}

```python
from homey import driver
from homey.pair_session import PairSession


class Driver(driver.Driver):
    async def on_pair(self, session: PairSession) -> None:
        async def on_pincode(pincode: list[str]) -> bool:
            # The pincode is given as an array of the filled in values
            return (
                pincode[0] == "1"
                and pincode[1] == "2"
                and pincode[2] == "3"
                and pincode[3] == "4"
            )


homey_export = Driver

```

{% endcode %}
{% endtab %}
{% endtabs %}


# Loading

This view will show a loading indicator. It's usually useful to show this view when an asynchronous operation needs to be made.

**Usage:** `"template": "loading"`

<figure><img src="/files/ZisTROl2wvLwvD5Qb2HS" alt=""><figcaption></figcaption></figure>

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "pair": [
    {
      "id": "list_devices",
      "template": "list_devices",
      "navigation": { "next": "loading" }
    },
    {
      "id": "loading",
      "template": "loading"
    },
    {
      "id": "add_devices",
      "template": "add_devices"
    }
  ]
}
```

{% endcode %}

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/drivers/\<driver\_id>/driver.js" %}

```javascript
const Homey = require("homey");
const DeviceAPI = require("device-api");

class Driver extends Homey.Driver {
  onPair(session) {
    session.setHandler("list_devices", async () => {
      return [
        {
          name: "My Device",
          data: {
            id: "abcd",
          },
        },
      ];
    });

    session.setHandler('showView', async (view) => {
      if (view === 'loading') {
        await DeviceAPI.connect();
        await session.nextView();
      }
    });
  }
}

module.exports = Driver;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/drivers/\<driver\_id>/driver.mts" %}

```mts
import Homey from "homey";
import DeviceApi from "./device-api.mjs";

export default class Driver extends Homey.Driver {
  async onPair(session: Homey.Driver.PairSession): Promise<void> {
    session.setHandler("list_devices", async (): Promise<object[]> => {
      return [
        {
          name: "My Device",
          data: {
            id: "abcd",
          },
        },
      ];
    });

    session.setHandler("showView", async (view: string): Promise<void> => {
      if (view === "loading") {
        await DeviceApi.connect();
        await session.nextView();
      }
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/drivers/\<driver\_id>/driver.py" %}

```python
from device_api import DeviceApi
from homey import driver
from homey.pair_session import PairSession


class Driver(driver.Driver):
    async def on_pair(self, session: PairSession) -> None:
        async def on_list_devices() -> list[driver.ListDeviceProperties]:
            return [{"name": "My Device", "data": {"id": "abcd"}}]

        async def on_show_view(view: str) -> None:
            if view == "loading":
                await DeviceApi.connect()
                await session.next_view()


homey_export = Driver

```

{% endcode %}
{% endtab %}
{% endtabs %}


# Done

This view will automatically close the pair session.

**Usage:** `"template": "done"`


# Custom Views

Most drivers will suffice using the provided templates. In certain cases you may want, or need, to create pairing screens that are more suited to your driver. For these cases it is possible to create custom pairing views, to learn more read the [custom pairing view guide](/advanced/custom-views/custom-pairing-views).

### Learn how to create custom pairing views

{% content-ref url="/pages/-MYtLHyrt9YP8RPcKPCu" %}
[Custom Pairing Views](/advanced/custom-views/custom-pairing-views)
{% endcontent-ref %}

### Learn how to style custom pairing views

The Homey Style Library is the key to a consistent user experience across all Homey Apps. We recommend to use this library above custom styling.

{% content-ref url="/pages/jcx6OKtRnkMWjOp0UaEH" %}
[HTML & CSS Styling](/advanced/custom-views/html-and-css-styling)
{% endcontent-ref %}


# Capabilities

A capability is a programmatic representation of a device's state.

A simple example of capabilities is `onoff`. This is a boolean capability that tells Homey whether the device is turned `on` (when `true`) or `off` (when `false`). Homey ships with many capabilities (called system capabilities). These can be found in the [Device Capability Reference](https://apps-sdk-v3.developer.homey.app/tutorial-device-capabilities.html).

## Using capabilities

In your App Manifest, for every driver an array `capabilities` is required. This is an array with the keys of all capabilities. This array can be overwritten during [pairing](/the-basics/devices/pairing#devices-list).

Your Device (`device.js`) instance needs to keep the device synchronised with Homey. Capabilities need to be synchronized both ways. This means that if a device's state changes, for example if the user turns on the lights, your app needs to tell Homey. It is also possible for Homey to request your app to change the state of the device, for example when a Flow is triggered to turn off the lights.

{% tabs %}
{% tab title="JavaScript" %}
Your `Device` class should listen for changes to the device's and then update the capability value within Homey by calling [`Device#setCapabilityValue()`](https://apps-sdk-v3.developer.homey.app/Device.html#setCapabilityValue). You should also register a method with [`Device#registerCapabilityListener()`](https://apps-sdk-v3.developer.homey.app/Device.html#registerCapabilityListener) that to update the state of the physical device.

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require('homey');
const DeviceApi = require('device-api');

class Device extends Homey.Device {
  async onInit() {
    this.registerCapabilityListener("onoff", async (value) => {
      await DeviceApi.setMyDeviceState({ on: value });
    });

    DeviceApi.on('state-changed', (isOn) => {
      this.setCapabilityValue('onoff', isOn).catch(this.error);
    });
  }
}

module.exports = Device;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
Your `Device` class should listen for changes to the device's and then update the capability value within Homey by calling [`Device#setCapabilityValue()`](https://apps-sdk-v3.developer.homey.app/Device.html#setCapabilityValue). You should also register a method with [`Device#registerCapabilityListener()`](https://apps-sdk-v3.developer.homey.app/Device.html#registerCapabilityListener) that to update the state of the physical device.

{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey from "homey";
import DeviceApi from "./device-api.mjs";

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    this.registerCapabilityListener("onoff", async (value: boolean) => {
      await DeviceApi.setMyDeviceState({ on: value });
    });

    DeviceApi.on("state-changed", (isOn: boolean) => {
      this.setCapabilityValue("onoff", isOn).catch(this.error);
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
Your `Device` class should listen for changes to the device's and then update the capability value within Homey by calling [`Device#set_capability_value()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.set_capability_value). You should also register a method with [`Device#register_capability_listener()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.register_capability_listener) that to update the state of the physical device.

{% code title="/drivers/\<driver\_id>/device.py" %}

```python
import asyncio

from device_api import DeviceApi
from homey import device


class Device(device.Device):
    async def on_init(self) -> None:
        async def onoff_listener(value: bool, **kwargs) -> None:
            await DeviceApi.set_my_device_state({"on": value})

        self.register_capability_listener("onoff", onoff_listener)

        def on_state_changed(is_on: bool) -> None:
            asyncio.create_task(
                self.set_capability_value("onoff", is_on)
            ).add_done_callback(
                lambda result: self.error(result.exception())
                if result.exception()
                else None
            )

        DeviceApi.on("state-changed", on_state_changed)


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

## Capability options

Some capabilities make use of capability options, which can be set to change the default behaviour of capabilities. Capability options can be set using the `capabilitiesOptions` object in the driver's entry in the App Manifest.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "class": "light",
  "capabilities": ["onoff", "dim"],
  "capabilitiesOptions": {
    "dim": { "preventInsights": true }
  }
}
```

{% endcode %}

Options that apply to all capabilities are:

| Attribute         | Description                                                                                                                                          |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `title`           | <p>Overwrite the capability title <code>{ "en": "My Custom Title" }</code>.<br><em>Make sure a custom title is never more than 2 - 3 words.</em></p> |
| `preventInsights` | Prevent Insights from being automatically generated.                                                                                                 |
| `preventTag`      | Prevent a Flow Tag from being automatically generated.                                                                                               |

### Duration

The duration capability option can be used to allow users to specify the duration of a Flow Action card for built-in capabilities. The configured duration will be passed as a second argument to your registered capability listener.

| Attribute  | Description                                                                                             |
| ---------- | ------------------------------------------------------------------------------------------------------- |
| `duration` | Set to `true` to allow users to set a duration on the Flow Action card associated with this capability. |

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require('homey');
const DeviceApi = require('device-api');

const DEFAULT_DIM_DURATION = 1000;

class Device extends Homey.Device {
  async onInit() {
    this.registerCapabilityListener("dim", async (value, options) => {
      await DeviceApi.setMyDeviceState({
        on: value,
        duration: typeof options.duration === "number"
          ? options.duration
          : DEFAULT_DIM_DURATION,
      });
    });
  }
}

module.exports = Device;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey from "homey";
import DeviceApi from "./device-api.mjs";

const DEFAULT_DIM_DURATION = 1000;

type DimOptions = {
  duration?: number;
};

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    this.registerCapabilityListener("dim", async (value: boolean, options: DimOptions) => {
      await DeviceApi.setMyDeviceState({ on: value, duration: options.duration ?? DEFAULT_DIM_DURATION });
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/drivers/\<driver\_id>/device.py" %}

```python
from device_api import DeviceApi
from homey import device

DEFAULT_DIM_DURATION = 1000


class Device(device.Device):
    async def on_init(self) -> None:
        async def dim_listener(value: bool, *, duration: int | None, **kwargs) -> None:
            await DeviceApi.set_my_device_state(
                {"on": value, "duration": duration or DEFAULT_DIM_DURATION}
            )

        self.register_capability_listener("dim", dim_listener)


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

### Boolean capability options

Options that apply to boolean capabilities, such as `onoff`, `windowcoverings_closed`, `garagedoor_closed,` `alarm_generic` and `button`, are:

<table data-header-hidden><thead><tr><th width="238">Attribute</th><th>Description</th></tr></thead><tbody><tr><td>Attribute</td><td>Description</td></tr><tr><td><code>insightsTitleTrue</code></td><td>A <a href="/pages/-MWdBAdwDRttE0KkWl48">translation object</a> which describes the title when shown in a Timeline.</td></tr><tr><td><code>insightsTitleFalse</code></td><td>A <a href="/pages/-MWdBAdwDRttE0KkWl48">translation object</a> which describes the title when shown in a Timeline.</td></tr><tr><td><code>titleTrue</code></td><td>A <a href="/pages/-MWdBAdwDRttE0KkWl48">translation object</a> which describes the title when shown in a sensor UI component.</td></tr><tr><td><code>titleFalse</code></td><td>A <a href="/pages/-MWdBAdwDRttE0KkWl48">translation object</a> which describes the title when shown in a sensor UI component.</td></tr></tbody></table>

### Number capability options

Options that apply to number capabilities, such as the `measure_*` capabilities, are:

<table data-header-hidden><thead><tr><th width="239">Attribute</th><th>Description</th></tr></thead><tbody><tr><td>Attribute</td><td>Description</td></tr><tr><td><code>units</code></td><td>A <a href="/pages/-MWdBAdwDRttE0KkWl48">translation object</a> of the capability's units, when applicable. If set to <code>"°C"</code> Homey can automatically convert this capability value to Fahrenheit.</td></tr><tr><td><code>decimals</code></td><td>The number of decimals to show in the UI.</td></tr><tr><td><code>min</code></td><td>A minimum for the capability value.</td></tr><tr><td><code>max</code></td><td>A maximum for the capability value.</td></tr><tr><td><code>step</code></td><td>A step size of the capability value.</td></tr></tbody></table>

### Enum capability options

Options that apply to enum capabilities, such as the `thermostat_mode,` are:

<table><thead><tr><th width="245">Attribute</th><th>Description</th></tr></thead><tbody><tr><td><code>values</code></td><td>An array of object's where each object contains a unique id and a title property that is a <a href="/pages/-MWdBAdwDRttE0KkWl48">translation object</a>.</td></tr></tbody></table>

```json
"capabilitiesOptions": {
    "thermostat_mode": {
      "values": [
        {
          "id": "heat",
          "title": { 
            "en": "Heat",
            "nl": "Verhitten"
          }
        },
        {
          "id": "cool",
          "title": { 
            "en": "Cool",
            "nl": "Koelen"
          }
        }
      ]
    }
  }
```

{% hint style="warning" %}
Note that these are only available since Homey v12.0.1 so in order to use this option increase your app's [compatibility](/the-basics/app/manifest#properties).
{% endhint %}

### Zone activity capability options

Certain capabilities will mark their device's zone active when their value changes. This behaviour can be controlled using capability options.

Options that apply to `alarm_motion`, `alarm_contact`, `alarm_vibration`, `alarm_occupancy` and `alarm_presence` are:

| Attribute      | Description                                                                                                                                            |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `zoneActivity` | Controls whether changes to this capability value also trigger the zone to become active. Set to `false` to disable zone activity for this capability. |

### Homey Energy capability options

Options that apply to `measure_power` are:

| Attribute      | Description                                                                                                                                                                                |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `approximated` | This capability option shows to the user that this power usage measurement might not be accurate. See [Energy](/the-basics/devices/energy#approximating-power-usage) for more information. |

Options that apply to `target_power` are:

| Attribute    | Description                                                                                                                       |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------- |
| `excludeMin` | Lower bound of the exclude range (number). Values between `excludeMin` and `excludeMax` become 0. Must satisfy `excludeMin <= 0`. |
| `excludeMax` | Upper bound of the exclude range (number). Values between `excludeMin` and `excludeMax` become 0. Must satisfy `excludeMax >= 0`. |

The `target_power` capability supports `excludeMin`/`excludeMax` options for devices with a minimum operating power (e.g., EV chargers that require at least 6A). Values inside the exclude range are automatically set to 0. For detailed examples and usage, see [Controlling target power usage](/the-basics/devices/energy#controlling-target-power-usage).

### Light device capability options

Options that apply to `onoff` are:

| Attribute  | Description                                                                                                                                                                                                                       |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setOnDim` | You can set this capability to `false` to prevent the `onoff` capability from being set when the `dim` capability is updated by a Flow action card. See [Lights](/the-basics/devices/best-practices/lights) for more information. |

### Getable

Options that apply to `onoff` and `volume_mute` are:

| Attribute | Description                                                                                                                                                                                                                                                         |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getable` | This capability option can be set to `false` to make the `onoff` or `volume_mute` capability stateless. If this option is set to `false` the device's `quickAction` will be disabled, the UI components will be updated, and some Flow cards will be added/removed. |

{% hint style="info" %}
The `getable` capability option is available as of Homey v7.2.1.
{% endhint %}

{% hint style="info" %}
Adding `getable: false` to an existing driver will break users' Flows as it removes a number of Flow cards belonging to the `onoff` and `volume_mute` capabilities.
{% endhint %}

## Custom capabilities

In some cases these might not suit your device. Your app can provide additional capabilities (called custom capabilities).

Define custom capabilities in your App Manifest, in an object `capabilities`.

{% code title="/.homeycompose/capabilities/my\_boolean\_capability.json" %}

```javascript
{
  "type": "boolean",
  "title": { "en": "My Boolean capability" },
  "getable": true,
  "setable": true,
  "uiComponent": "toggle",
  "uiQuickAction": true,
  "icon": "/assets/my_boolean_capability.svg"
}
```

{% endcode %}

{% code title="/.homeycompose/capabilities/my\_numeric\_capability.json" %}

```javascript
{
  "type": "number",
  "title": { "en": "My Numeric capability" },
  "uiComponent": "slider",
  "getable": true,
  "setable": false,
  "units": { "en": "Cb" },
  "min": 0,
  "max": 30,
  "step": 0.5
}
```

{% endcode %}

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "class": "other",
  "capabilities": ["onoff", "my_boolean_capability", "my_numeric_capability"]
}
```

{% endcode %}

The following options can be set for all custom capabilities:

When the capability type is `boolean` the following additional properties can be set:

| Property             | Description                                                                                                      |
| -------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `uiQuickAction`      | Set this to true when you want the user to quick-toggle the capability's value from the UI.                      |
| `insightsTitleTrue`  | A [translation object](/the-basics/app/internationalization) which describes the title when shown in a Timeline. |
| `insightsTitleFalse` | A [translation object](/the-basics/app/internationalization) which describes the title when shown in a Timeline. |

When the capability type is `number` the following additional properties can be set:

| Property   | Description                               |
| ---------- | ----------------------------------------- |
| `min`      | A minimum for the capability value.       |
| `max`      | A maximum for the capability value.       |
| `step`     | A step size of the capability value.      |
| `decimals` | The number of decimals to show in the UI. |

When the capability type is `enum` the following additional properties can be set:

| Property | Description                                                                                                                                                                                                                                                                                                                      |
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `values` | An array of possible values for this capability. A value consists of an `id`, which will be the capability value, and a `title`, which is a [translation object](/the-basics/app/internationalization). The three `values` should have the ID's `up`, `idle`, and `down`.`{ "id": "option1", "title": { "en": "First option" }}` |

{% hint style="warning" %}
The `values` for the `target_power_mode` capability may be customized. When providing a custom `values` array, you must include `homey` and at least one non-`homey` value. The default `device` value can be omitted if you define your own strategy values (e.g. `self_use`, `price_based`). The prefix `homey_` is reserved and cannot be used.
{% endhint %}

### Device Indicators and Custom Capabilities

The Homey web app and mobile app can display indicators next to the device icons. This gives users the ability to view a specific capability, such as a temperature value or battery status, at a glance. Custom boolean and number capabilities can also be shown as indicators as device indicators.

#### Boolean Capabilities

Boolean capabilities, also called Alarms in Homey, are displayed in two different ways if they start with the prefix `alarm_`. By default all capabilities with this prefix are grouped, and a warning icon is shown if the value of any of the boolean capabilities is `true`. Alternatively users can choose to show the indicator value of a single specific Alarm capability. The `alarm_battery` capabilities will show an "empty battery" icon instead of an exclamation mark.

#### Number Capabilities

Number capabilities, are displayed as a numeric value together with the unit of the capability. Users are able to select capabilities that start with either the prefix `measure_` or `meter_`. The `measure_battery` capability will always be displayed with a custom battery icon instead of a number.

![An example with capabilities: 'meter\_power', 'measure\_battery' and 'alarm\_motion'.](/files/-MbfRZhOLwtdW7_jTrHI)

{% hint style="warning" %}
Note that it's not possible for users to select and override the default indicator if the device class is **thermostat**, **light**, **lock** or **speaker**.
{% endhint %}

## UI Components

All system capabilities have their own UI component. Custom capabilities can also use these UI components. Homey will automatically try to find the right component, but you can override this by specifying the `uiComponent` property in your custom capability.

### Toggle

`"uiComponent": "toggle"`

The toggle component displays one `boolean` capability. Depending on the capability, the look might change.

<figure><img src="/files/VUCu9fAsHN43yFy2iVHQ" alt=""><figcaption></figcaption></figure>

### Slider

`"uiComponent": "slider"`

The slider component displays one `number` capability. Depending on the capability, the look might change.

<figure><img src="/files/VcfL5q7MuKuLubqnbBtz" alt=""><figcaption></figcaption></figure>

### Sensor

`"uiComponent": "sensor"`

The sensor component displays multiple `number`, `enum`, `string` or `boolean` capabilities.

Booleans that are `true` and begin with `alarm_` will flash red.

<figure><img src="/files/2gMsVSqySCqIfE8d0qn8" alt=""><figcaption></figcaption></figure>

### Thermostat

`"uiComponent": "thermostat"`

The thermostat component displays a `target_temperature` capability, and an optional `measure_temperature`.

If you use sub-capabilities for `target_temperature` and `measure_temperature`, make sure the dot suffix of the capabilities are the same so that they will be displayed together, for instance: `target_temperature.top` and `measure_temperature.top`.

<figure><img src="/files/wBdZFYzqbGPvy9imUC2u" alt=""><figcaption></figcaption></figure>

### Media

`"uiComponent": "media"`

The media component accepts the `speaker_playing`, `speaker_next`, `speaker_prev`, `speaker_shuffle` and `speaker_repeat` capabilities.

Additionally, it shows the album art as set using [`Device#setAlbumArt()`](https://apps-sdk-v3.developer.homey.app/Device.html#setAlbumArt) or [`Device#set_album_art_image()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.set_album_art_image).

<figure><img src="/files/kQha5JB7J9SKP35t2lkr" alt=""><figcaption></figcaption></figure>

### Color

`"uiComponent": "color"`

The color component accepts the `light_hue`, `light_saturation`, `light_temperature` and `light_mode` capabilities.

<figure><img src="/files/0uiJGVfmltkIptmMmrcR" alt=""><figcaption></figcaption></figure>

### Battery

`"uiComponent": "battery"`

The battery component accepts either a `measure_battery` or `alarm_battery` capability.

<figure><img src="/files/pbAvgKLXQrAh2Bnbyw8n" alt=""><figcaption></figcaption></figure>

### Picker

`"uiComponent": "picker"`

The picker component accepts one `enum` capability and shows a list of possible values. Make sure the values titles fit on one line and are never more than 3 words.

<figure><img src="/files/8wjYZChpKP9jou42cPIM" alt=""><figcaption></figcaption></figure>

### Ternary

`"uiComponent": "ternary"`

The ternary component accepts one `enum` capability with three values, meant for motorized components.

<figure><img src="/files/1cBDTYDmnccpRf0ZfQD8" alt=""><figcaption></figcaption></figure>

### Button

`"uiComponent": "button"`

The button component displays one or more `boolean` capabilities. Depending on the capability, the look might change. Most buttons are stateless but it is possible to add a stateful button if the capability is both `setable` and `getable`. Some buttons will be grouped together like volume\_up and volume\_down.

<figure><img src="/files/GUfzPbKhlnVujwCHTANy" alt=""><figcaption></figcaption></figure>

### *No UI component*

`"uiComponent": null`

To hide the UI component, specify `null` as value.

## Maintenance Actions

> This feature depends on Homey v3.1.0 and Homey Smartphone App v3.0.1.

Button capabilities can be flagged as maintenance action. This will show a button in 'Device settings > Maintenance actions' and hide the `uiComponent` in the device view. When this button is pressed the registered capability listener will be triggered. This allows you to initiate actions from the device's settings.

Example use cases:

* Starting the calibration process on a device
* Resetting accumulated power measurements

<figure><img src="/files/EjGlhaylvEgFMgTmesJi" alt=""><figcaption></figcaption></figure>

### Creating a maintenance action

A maintenance action capability must be a capability that extends the system capability `button`. In order to mark it as a maintenance action add the `maintenanceAction: true` property to the `capabilitiesOptions` object of the driver manifest. Additionally, provide a `title` property, and optionally a `desc` property.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "P1 Meter" },
  "capabilities": [
    "meter_power",
    "measure_power",
    "button.calibrate",
    "button.reset_meter"
  ],
  "capabilitiesOptions": {
    "button.calibrate": {
      "maintenanceAction": true,
      "title": { "en": "Start calibration" },
      "desc": { "en": "Start the sensor calibration process." }
    },
    "button.reset_meter": {
      "maintenanceAction": true,
      "title": { "en": "Reset power meter" },
      "desc": { "en": "Reset the accumulated power usage (kWh), this can not be restored." }
    }
  }
}
```

{% endcode %}

### Listening for maintenance action events

{% tabs %}
{% tab title="JavaScript" %}
Register the capability listeners in `device.js` to listen for maintenance action events.

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require('homey');

class Device extends Homey.Device {
  async onInit() {
    this.registerCapabilityListener('button.reset_meter', async () => {
      // Maintenance action button was pressed
    });

    this.registerCapabilityListener('button.calibrate', async () => {
      // Maintenance action button was pressed, return a promise
      throw new Error('Something went wrong');
    });
  }
}

module.exports = Device;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
Register the capability listeners in `device.mts` to listen for maintenance action events.

{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey from "homey";

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    this.registerCapabilityListener("button.reset_meter", async (): Promise<void> => {
      // Maintenance action button was pressed
    });

    this.registerCapabilityListener("button.calibrate", async (): Promise<never> => {
      // Maintenance action button was pressed, return a promise
      throw new Error("Something went wrong");
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
Register the capability listeners in `device.py` to listen for maintenance action events.

{% code title="/drivers/\<driver\_id>/device.py" %}

```python
from homey import device


class Device(device.Device):
    async def on_init(self) -> None:
        async def reset_meter_listener(value: bool, **kwargs) -> None:
            # Maintenance action button was pressed
            ...

        self.register_capability_listener("button.reset_meter", reset_meter_listener)

        async def calibrate_listener(value: bool, **kwargs) -> None:
            # Maintenance action button was pressed, return a promise
            raise Exception("Something went wrong")

        self.register_capability_listener("button.calibrate", calibrate_listener)


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

## Sub-capabilities - using the same capability more than once

In certain cases it might occur that a device should use a capability more than once. You can use a sub-capability for this purpose.

An example would be a device with an outside and inside temperature sensor. Simply append a dot followed by an identifier after the capability string during in your driver, e.g. `measure_temperature.inside` & `measure_temperature.outside`.

{% hint style="warning" %}
Flow Cards will not be automatically generated for sub-capabilities, you should create these cards yourself.
{% endhint %}


# Energy

By defining the energy (kWh) and power (W) usage or generation of the devices your app supports, Homey can provide detailed reports and insights into their energy consumption.

<figure><img src="/files/bB2qN1lqoyXZpkZbZIhy" alt=""><figcaption><p>Energy overview</p></figcaption></figure>

## Capabilities

Devices in Homey can either consume energy (e.g. a light bulb), generate energy (e.g. solar panels) or measure the home's total usage (e.g. a P1 meter or current clamp). For this the `measure_power` and `meter_power` capabilities are used by *Energy*.

### Measure Power

The `measure_power` capability represents the instantaneous power usage or generation of a device, measured in watts (W). It indicates how much power a device is currently consuming or producing at a given moment.

### Meter Power

The `meter_power` capability (or `meter_power` sub capabilities) represents energy in kilowatt-hours (kWh) and can be used in two ways:

1. **Cumulative energy**

Tracks the **total** amount of energy consumed or generated over time, with values that continuously and only increase. It is typically reset only when the device is reset or reinstalled. *Example***:** A smart meter displaying 12,345 kWh since installation.

2. **Non-cumulative**

Represents energy used or generated during a **specific time interval**, without accumulating past values. *Example***:** Energy consumed in the last 24 hours, or current battery state of charge in kWh.

In the following sections, when `meter_power` is referenced, it always refers to **cumulative** energy, unless explicitly stated otherwise. In this context, `meter_power` tracks the total energy consumed or generated over time, measured in kilowatt-hours (kWh).

*Energy* calculates energy consumption over a given period by analyzing the difference in the `meter_power` value over time. If the values are periodically reset to zero or decrease unexpectedly, it may lead to data loss or invalid interpretations.

### Target Power

The `target_power` capability allows Homey to control the power consumption or production of devices in Watts. This enables energy management scenarios such as:

* Solar power curtailment
* Smart EV charging
* Controlling Home Batteries

The `target_power` capability automatically generates a Flow card action "Set target power".

{% hint style="info" %}
The `target_power` capability is available as of Homey v12.13.0.
{% endhint %}

### Target Power Mode

The `target_power_mode` capability controls whether Homey or the device itself is in charge of power management. Adding this capability is optional and only useful when the device has its own smart logic such as internal scheduling, self-consumption optimization, cloud control, or app-based control. Without `target_power_mode`, Homey assumes full control at all times, which is suitable for simple devices without built-in power management.

The `target_power_mode` capability automatically generates Flow cards for triggers, actions, and conditions.

{% hint style="info" %}
The `target_power_mode` capability is available as of Homey v12.13.0.
{% endhint %}

## Energy configuration

Throughout this document the `energy` configuration is often referred to. It describes the `energy` configuration object as defined in the drivers manifest, for example:

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```json
{
  "name": { "en": "My Driver" },
  "energy": {
    "meterPowerImportedCapability": "meter_power.imported",
    "meterPowerExportedCapability": "meter_power.exported"
  }
}
```

{% endcode %}

This object is used to set various properties defined in the sections below. By default any changes made to the `energy` configuration in this file will be applied directly to existing and already connected devices. However, this is not the case when the device has applied an `energy` configuration override using [`Device.setEnergy()`](https://apps-sdk-v3.developer.homey.app/Device.html#setEnergy) or [`Device.set_energy()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.set_energy) (see [#dynamically-changing-the-energy-configuration](#dynamically-changing-the-energy-configuration "mention")for more details).

To access the `energy` configuration as set in this file use the [`Driver.manifest`](https://python-apps-sdk-v3.developer.homey.app/driver.html#homey.driver.Driver.manifest) property.

## Dynamically changing the energy configuration

In some cases, you need to set the `energy` configuration dynamically. For example when the required properties depend on the specific device or its capabilities which are not known upfront. In this case it is possible to **override** the `energy` configuration from `driver.compose.json`.

{% tabs %}
{% tab title="JavaScript" %}
To set the `energy` configuration dynamically use [`Device.setEnergy()`](https://apps-sdk-v3.developer.homey.app/Device.html#setEnergy) . You must provide the complete `energy` configuration to [`Device.setEnergy()`](https://apps-sdk-v3.developer.homey.app/Device.html#setEnergy) as it will overwrite all the existing properties. Note that once [`Device.setEnergy()`](https://apps-sdk-v3.developer.homey.app/Device.html#setEnergy) is called, the device will disregard all properties set in the `energy` configuration in `driver.compose.json`.

When required to restore to the `energy` configuration from `driver.compose.json` after using [`Device.setEnergy()`](https://apps-sdk-v3.developer.homey.app/Device.html#setEnergy) it is possible to call [`Device.setEnergy()`](https://apps-sdk-v3.developer.homey.app/Device.html#setEnergy) with the original `energy` configuration as read from the driver's manifest. However, once [`Device.setEnergy()`](https://apps-sdk-v3.developer.homey.app/Device.html#setEnergy) has been used, any changes made to the energy configuration in `driver.compose.json` will no longer be applied automatically.

Use [`Device.getEnergy()`](https://apps-sdk-v3.developer.homey.app/Device.html#getEnergy) to get the `energy` configuration override as set by [`Device.setEnergy()`](https://apps-sdk-v3.developer.homey.app/Device.html#setEnergy) . Note that this will **not** return the `energy` configuration from `driver.compose.json`, but only the configuration set with [`Device.setEnergy()`](https://apps-sdk-v3.developer.homey.app/Device.html#setEnergy) .

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const DeviceApi = require("device-api");
class MyDevice extends Homey.Device {
  async onInit() {
    const energyConfig = this.getEnergy();
    DeviceApi.on("energy-settings", (energySettings) => {
      if (energySettings.isSmartMeter() && energyConfig.cumulative !== true) {
        this.setEnergy({
          cumulative: true,
          cumulativeImportedCapability: "meter_power.imported",
        }).catch(this.error);
      }
    });
  }
}
module.exports = MyDevice;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
To set the `energy` configuration dynamically use [`Device.setEnergy()`](https://apps-sdk-v3.developer.homey.app/Device.html#setEnergy) . You must provide the complete `energy` configuration to [`Device.setEnergy()`](https://apps-sdk-v3.developer.homey.app/Device.html#setEnergy) as it will overwrite all the existing properties. Note that once [`Device.setEnergy()`](https://apps-sdk-v3.developer.homey.app/Device.html#setEnergy) is called, the device will disregard all properties set in the `energy` configuration in `driver.compose.json`.

When required to restore to the `energy` configuration from `driver.compose.json` after using [`Device.setEnergy()`](https://apps-sdk-v3.developer.homey.app/Device.html#setEnergy) it is possible to call [`Device.setEnergy()`](https://apps-sdk-v3.developer.homey.app/Device.html#setEnergy) with the original `energy` configuration as read from the driver's manifest. However, once [`Device.setEnergy()`](https://apps-sdk-v3.developer.homey.app/Device.html#setEnergy) has been used, any changes made to the energy configuration in `driver.compose.json` will no longer be applied automatically.

Use [`Device.getEnergy()`](https://apps-sdk-v3.developer.homey.app/Device.html#getEnergy) to get the `energy` configuration override as set by [`Device.setEnergy()`](https://apps-sdk-v3.developer.homey.app/Device.html#setEnergy) . Note that this will **not** return the `energy` configuration from `driver.compose.json`, but only the configuration set with [`Device.setEnergy()`](https://apps-sdk-v3.developer.homey.app/Device.html#setEnergy) .

{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey from "homey";
import DeviceApi from "device-api";

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    const energyConfig = this.getEnergy();
    DeviceApi.on("energy-settings", energySettings => {
      if (energySettings.isSmartMeter() && energyConfig.cumulative !== true) {
        this.setEnergy({
          cumulative: true,
          cumulativeImportedCapability: "meter_power.imported",
        }).catch(this.error);
      }
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
To set the `energy` configuration dynamically use [`Device.set_energy()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.set_energy) . You must provide the complete `energy` configuration to [`Device.set_energy()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.set_energy) as it will overwrite all the existing properties. Note that once [`Device.set_energy()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.set_energy) is called, the device will disregard all properties set in the `energy` configuration in `driver.compose.json`.

When required to restore to the `energy` configuration from `driver.compose.json` after using [`Device.set_energy()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.set_energy) it is possible to call [`Device.set_energy()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.set_energy) with the original `energy` configuration as read from the driver's manifest. However, once [`Device.set_energy()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.set_energy) has been used, any changes made to the energy configuration in `driver.compose.json` will no longer be applied automatically.

Use [`Device.get_energy()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.get_energy) to get the `energy` configuration override as set by [`Device.set_energy()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.set_energy) . Note that this will **not** return the `energy` configuration from `driver.compose.json`, but only the configuration set with [`Device.set_energy()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.set_energy) .

{% code title="/drivers/\<driver\_id>/device.py" %}

```python
import asyncio

from device_api import DeviceApi
from homey import device


class Device(device.Device):
    async def on_init(self) -> None:
        energy_config = self.get_energy()

        async def on_energy_Settings(energy_settings) -> None:
            if energy_settings.is_smart_meter() and not energy_config.get("cumulative"):
                try:
                    await self.set_energy(
                        {
                            "cumulative": True,
                            "cumulativeImportedCapability": "meter_power.imported",
                        }
                    )
                except Exception as e:
                    self.error(e)

        DeviceApi.on(
            "address-changed", lambda x: asyncio.create_task(on_energy_Settings(x))
        )


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

{% hint style="info" %}
Try not to use these methods too often, as they are quite impactful. They should only be used when initially configuring the device.
{% endhint %}

## Power

Devices in Homey can consume power, for example a light bulb or TV, and generate power, for example solar panels or home batteries (by discharging). There are two strategies to determine power usage:

1. **Measuring Power Usage**\
   When the device itself provides actual power measurements ( [#measuring-power-usage](#measuring-power-usage "mention")).
2. **Approximating Power Usage**\
   When a device does not provide actual power measurements, its can be estimated in two ways:
   1. **Using configurable power usage properties**\
      Define static values in the device's energy configuration (e.g., `usageConstant`, `usageOff`, or `usageOn`) to approximate consumption based on the device's state ([#constant-power-usage](#constant-power-usage "mention")).
   2. **Using approximated `measure_power` values**\
      Provide values for the `measure_power` capability and explicitly mark them as approximations using the `measure_power.approximation` flag ([#dynamic-power-usage](#dynamic-power-usage "mention")). This indicates that the values are not based on actual measurements.

### **Measuring power usage**

When a device supports the `measure_power` capability (real-time power usage in Watts), such as a smart socket, Homey automatically uses this value for that device.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```json
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "class": "socket",
  "capabilities": ["onoff", "measure_power"]
}
```

{% endcode %}

### Approximating power usage

In cases where devices do not support `measure_power` real-time power usage in Watts, there are a few options to approximate the device's power consumption. Homey can calculate the power usage based on the `onoff` and `dim` capabilities when there is no `measure_power` capability available. Homey will provide the user with settings to configure the power consumption when the device is turned on, turned off, or (when a device can't be turned on or off) its constant power consumption. Using these settings Homey approximates the power usage by calculating the total on-time, optionally taking into account the brightness level.

#### Constant power usage

If you already know the power consumption of the device (often this can be found on the packaging or the device itself) add the `usageOn`and `usageOff` properties to the `energy.approximation` object of your driver's manifest. Below you can find an example of a light bulb driver that consumes 15W when turned on, and 1W when turned off. This eliminates the need for the user to configure it manually.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```json
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "platforms": ["local", "cloud"],
  "connectivity": "zigbee",
  "class": "light",
  "capabilities": ["onoff", "dim"],
  "energy": {
    "approximation": {
      "usageOn": 15, // in Watt
      "usageOff": 1 // in Watt
    }
  }
}
```

{% endcode %}

{% hint style="info" %}
When a device has a stand-by function, use the stand-by value for `usageOff`.
{% endhint %}

Some devices, such as a router, use a constant amount of power. In this case you can add the `usageConstant` property.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```json
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "energy": {
    "approximation": {
      "usageConstant": 5 // in Watt
    }
  }
}
```

{% endcode %}

{% hint style="info" %}
Keep in mind that a user can always overwrite these values under the device's settings.
{% endhint %}

#### Dynamic power usage

The power usage of some devices depends on their configuration. For example, Nanoleaf light panels let the user add more panels to the system, increasing the total power consumption of the device.

In these kind of scenarios you need to add the `measure_power` capability with the `approximated: true` flag as capability option. Then programmatically calculate and update the `measure_power` value yourself.

By adding the `approximated: true` flag the user will be shown that this value is an approximation and not a precisely measured value.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```json
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "class": "sensor",
  "capabilities": ["onoff", "measure_power"],
  "capabilitiesOptions": {
    "measure_power": {
      "approximated": true
    }
  }
}
```

{% endcode %}

### Controlling target power usage

The `target_power` capability allows Homey to control the power consumption or production of devices in Watts. This enables energy management scenarios such as:

* Solar power curtailment
* Smart EV charging
* Controlling Home Batteries

{% hint style="info" %}
For device-specific `target_power` configuration and driver examples, see [Solar panels](#solar-panels), [EV chargers](#ev-chargers-1), and [Home batteries](#home-batteries).
{% endhint %}

The `target_power` capability is a setable number capability measured in Watts (W). It represents the desired power level that a device should consume or produce. The actual power consumption is reported via the `measure_power` capability.

* **Positive values** = power consumption (charging), or maximum allowed production (solar curtailment)
* **Negative values** = power production (discharging)
* **Zero** = idle, or full curtailment (solar)

When the device cannot achieve the requested target power, the driver should throw an error from the capability listener.

For devices with a minimum operating power (like EV chargers requiring 6A minimum), use the [`excludeMin`/`excludeMax`](/the-basics/devices/capabilities#homey-energy-capability-options) capability options to define a range around zero where the device cannot operate. Values inside this range are automatically set to 0.

{% hint style="info" %}
**Range must include zero:** The `min`/`max` range of `target_power` must always include 0 (`min <= 0 <= max`). All devices need the ability to idle. If your device has a minimum operating threshold, use `excludeMin`/`excludeMax` instead of setting `min` above zero.
{% endhint %}

{% hint style="info" %}
**Exclude range validation:** The exclude range must include 0 (`excludeMin <= 0 <= excludeMax`). Values exactly at the boundaries are valid; only values strictly between them become 0.
{% endhint %}

{% hint style="info" %}
**Step rounding:** Values are rounded **toward zero** to the nearest `step`, preventing Homey from requesting more power than intended. Values inside the exclude range become 0. Values outside the `min`/`max` range are clamped to the nearest boundary.
{% endhint %}

The `target_power_mode` capability controls whether Homey or the device itself is in charge of power management. Adding this capability is optional.

**When to use:** Add `target_power_mode` when the device has its own smart logic such as internal scheduling, self-consumption optimization, cloud control, or app-based control. Without `target_power_mode`, Homey assumes full control at all times, which is suitable for simple devices without built-in power management.

| Value    | Description                                                                                                                                                                                                              |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `device` | **Device in control.** The device operates autonomously using its own internal logic (e.g., built-in scheduling, self-consumption optimization). Target power values set by Homey are ignored. This is the default mode. |
| `homey`  | **Homey in control.** Homey actively controls the device via `target_power`. The device should execute exactly what Homey requests, disabling any built-in smart logic.                                                  |

When using the **Set target power** Flow card, Homey automatically switches `target_power_mode` to `homey`.

When switching from `homey` to `device` mode, the driver should discard any `target_power` setpoint and resume internal device logic.

{% hint style="warning" %}
The `values` for the `target_power_mode` capability may be customized. When providing a custom `values` array, you must include `homey` and at least one non-`homey` value. The default `device` value can be omitted if you define your own strategy values (e.g. `self_use`, `price_based`). The prefix `homey_` is reserved and cannot be used.
{% endhint %}

{% hint style="info" %}
**Custom strategy modes:** If your device has multiple operational strategies (e.g. self-consumption, price-based optimization), you can replace the generic `device` value with specific strategy values. Any non-`homey` value means the device controls its own power. Example:

```json
"target_power_mode": {
  "values": [
    { "id": "homey", "title": { "en": "Homey" } },
    { "id": "self_use", "title": { "en": "Self-use" } },
    { "id": "price_based", "title": { "en": "Price-based" } }
  ]
}
```

{% endhint %}

#### Example

For device-specific implementation examples, see [Solar panels](#solar-panels), [EV chargers](#ev-chargers-1), and [Home batteries](#home-batteries).

For devices with `target_power_mode` (e.g. home batteries), use `registerMultipleCapabilityListener` to handle both capabilities as a single operation.

You can also use separate `registerCapabilityListener` calls per capability. This is simpler but means the driver receives each change individually, potentially sending multiple API calls to the device for what is logically a single operation.

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require("homey");

class MyDevice extends Homey.Device {
  async onInit() {
    this.registerMultipleCapabilityListener(
      ["target_power", "target_power_mode"],
      async ({ target_power, target_power_mode }) => {
        // Only the changed capabilities are present in the object
        if (target_power_mode === "device") {
          // Device should resume autonomous operation
          await this.enableBuiltInScheduling();
          this.log("Switched to device mode - device controls itself");
          return;
        }

        if (target_power_mode === "homey") {
          // Homey is now controlling the device
          await this.disableBuiltInScheduling();
          this.log("Switched to homey mode");
        }

        // Apply target power (use changed value or fall back to current)
        const value =
          target_power ?? this.getCapabilityValue("target_power") ?? 0;
        const mode =
          target_power_mode ?? this.getCapabilityValue("target_power_mode");

        if (mode !== "homey") {
          this.log("Ignoring target_power - not in homey mode");
          return;
        }

        await this.applyTargetPower(value);
      },
      500, // debounce timeout in ms
    );
  }

  async applyTargetPower(value) {
    if (value >= 0) {
      await this.setChargingPower(value);
    } else {
      await this.setDischargingPower(Math.abs(value));
    }
  }
}

module.exports = MyDevice;
```

{% endcode %}

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require('homey');
const DeviceApi = require('device-api');
​
class MyDevice extends Homey.Device {
  async onInit() {
    DeviceApi.on('power-usage-changed', (watts) => {
      this.setCapabilityValue('measure_power', watts).catch(this.error);
    });
  }
}
​
module.exports = MyDevice;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey from "homey";
import DeviceApi from "./device-api.mjs";

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    DeviceApi.on("power-usage-changed", (watts: number) => {
      this.setCapabilityValue("measure_power", watts).catch(this.error);
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/drivers/\<driver\_id>/device.py" %}

```python
import asyncio

from device_api import DeviceApi
from homey import device


class Device(device.Device):
    async def on_init(self) -> None:
        async def on_power_usage_changed(watts: float) -> None:
            try:
                await self.set_capability_value("measure_power", watts)
            except Exception as e:
                self.error(e)

        DeviceApi.on(
            "power-usage-changed",
            lambda x: asyncio.create_task(on_power_usage_changed(x)),
        )


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

## Energy

Devices in Homey can report energy (kWh) consumption, for example a washing machine, and energy generation, for example solar panels or home batteries (by discharging).\
\
These devices should have the `meter_power` capability (cumulative energy usage in kWh). For example, a washing machine that tracks its energy consumption would use the `meter_power` capability to keep track of how much energy is consumed over longer periods of time.

If a device can measure both imported and exported energy, such as a smart plug connected to portable solar panels or a home battery, the driver should define the `meterPowerImportedCapability` and `meterPowerExportedCapability` energy properties, as shown below.

* **Imported energy** refers to the energy consumed or charged by the device.
* **Exported energy** refers to the energy produced or discharged by the device.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```json
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "class": "socket",
  "capabilities": [
    "onoff",
    "measure_power",
    "meter_power.imported",
    "meter_power.exported"
  ],
  "energy": {
    "meterPowerImportedCapability": "meter_power.imported",
    "meterPowerExportedCapability": "meter_power.exported"
  }
}
```

{% endcode %}

{% hint style="info" %}
You can set any capability as the value of `meterPowerImportedCapability` and `meterPowerExportedCapability` as long it is an instance of the `meter_power` capability and is present in the `capabilities` array of the driver.
{% endhint %}

{% hint style="info" %}
The `meterPowerImportedCapability` and `meterPowerExportedCapability` are only used on Homey Pro (Early 2023), Homey Pro mini, and Homey Cloud and are available as of v12.4.5.
{% endhint %}

## Devices

There are various types of devices that have some special features or requirements in *Energy*.

{% hint style="warning" %}
**Multi-purpose devices require separate Homey devices**

When integrating hardware that combines multiple energy functions (e.g., an EV charger with built-in solar inverter, or a hybrid inverter with battery storage), you must create separate Homey devices for each function, each with its own device class.

This separation is required because each device class is treated differently in Energy.
{% endhint %}

### Solar panels

Solar panels are a common device in Homey that can generate their own power. These devices must have the `solarpanel` device class.

#### Measure & meter power

The `measure_power` value should **positive** when generating power. When providing a negative value, e.g. `-13` watt, Homey assumes the solar panel is currently consuming instead of generating power.

In order for cumulative energy generation (kWh) to be tracked in *Energy* the driver must have a `meter_power` capability that will be set to the total generated energy in kWh as a *positive* value. Use the `meterPowerExportedCapability` energy property to configure a different `meter_power` capability for generated energy.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```json
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "class": "solarpanel",
  "capabilities": ["measure_power", "meter_power"],
  "energy": {
    "meterPowerExportedCapability": "meter_power" // Optional: defaults to meter_power
  }
}
```

{% endcode %}

#### Target power

Solar inverters with curtailment support can use `target_power` to limit their power output and optionally `target_power_mode` to determine who controls the `target_power`. This is useful when grid export limits apply or when energy prices are negative.

For solar inverters, `target_power` acts as a **maximum production limit** (cap), not a production target. Setting `target_power` to 1500W means "produce up to 1500W maximum".

**To disable curtailment:** Set `target_power` to its maximum value (`max`). The driver should interpret this as "produce at maximum capacity".

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```json
{
  "name": { "en": "Solar Inverter" },
  "class": "solarpanel",
  "capabilities": [
    "measure_power",
    "meter_power",
    "target_power",
    "target_power_mode"
  ],
  "capabilitiesOptions": {
    "target_power": {
      "min": 0,
      "max": 10000
    }
  }
}
```

{% endcode %}

Optionally add the `target_power_mode` capability:

* In **device** mode, the inverter produces at maximum capacity using its own logic
* In **homey** mode, Homey controls curtailment:
  * Set to **5000** → limit production to 5000W
  * Set to **10000** (max) → no curtailment, produce at full capacity
  * Set to **0** → full curtailment, stop producing

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require("homey");

class MySolarInverter extends Homey.Device {
  async onInit() {
    this.registerCapabilityListener("target_power", async (value) => {
      const mode = this.getCapabilityValue("target_power_mode");
      if (mode !== "homey") return;
      await this.applyTargetPower(value);
    });

    this.registerCapabilityListener("target_power_mode", async (mode) => {
      if (mode === "homey") {
        const value = this.getCapabilityValue("target_power");
        await this.applyTargetPower(value);
      } else {
        // "device" mode: disable curtailment, produce at maximum
        await this.disableCurtailment();
      }
    });
  }

  async applyTargetPower(value) {
    const capabilityOptions = this.getCapabilityOptions("target_power");
    const maxPower = capabilityOptions.max;

    if (value >= maxPower) {
      // Max value means "no curtailment"
      await this.disableCurtailment();
      this.log("Curtailment disabled, producing at maximum");
    } else {
      await this.setCurtailmentLimit(value);
      this.log(`Curtailment set to ${value}W`);
    }
  }
}

module.exports = MySolarInverter;
```

{% endcode %}

### Smart plugs

Devices with device class `socket` (often smart plugs), can measure power and energy being consumed and generated. To learn how to configure this see [#measuring-power-usage](#measuring-power-usage "mention").\
\
A user can choose a different device class in the *What's plugged in?* setting. Among others, `solarpanel`, `battery` and `evcharger`. To properly support these three device classes please take the following into account.

#### Solar panels

Important difference with the regular `solarpanel` device class is that the generated power must be set as a **negative** value (e.g. `setCapabilityValue('measure_power', -200)`). Homey will then invert this value automatically.

{% hint style="info" %}
Users will get the [Invert power measurement](https://apps.developer.homey.app/the-basics/devices/energy#invert-power-measurement) setting to manually switch the generated power from positive to negative or vice versa.
{% endhint %}

Homey will use the value of the `meter_power` capability to determine the energy generated by the solar panels in kWh. Use the `meterPowerExportedCapability` energy property to use a different `meter_power` capability for generated energy (see [#solar-panels](#solar-panels "mention")).

#### Batteries

Small portable home batteries can be connected to a smart plug in order to charge and discharge.

The `measure_power` value must be positive when the smart plug is consuming power (charging the battery) and negative when producing power (discharging the battery).

By default the `meter_power` capability of the smart plug will be used to determine the energy charged by the battery. Additionally, for the charged and discharged energy to be registered separately by Homey, smart plug drivers must include the `meterPowerImportedCapability` and `meterPowerExportedCapability` energy properties (see [#home-batteries](#home-batteries "mention")).

#### EV chargers

Users can charge EVs using smart plugs. To allow the smart plug to act as an EV charger in *Energy* the user can choose "EV Charger" as device class in the *What's plugged in?* setting of smart plugs. This will include the smart plug's power measurements in *Energy* as if it were an EV charger.

The `measure_power` value must be positive when the smart plug is consuming power (charging the EV's battery) and negative when producing power (discharging the EV's battery).

By default the `meter_power` capability of the smart plug will be used to determine the energy charged by the EV charger. Additionally, for the charged and (if applicable) discharged energy to be registered separately by Homey, smart plug drivers must include the `meterPowerImportedCapability` and `meterPowerExportedCapability` energy properties (see [#ev-chargers-1](#ev-chargers-1 "mention")).

### Cumulative measuring devices

Certain devices, such as a P1 meter or a current clamp, can measure the total power and energy usage of a home or a specific power group. Their measurements contribute to the overall power consumption data for the entire home. This means that these are the highest level measuring devices in a home. All other power consuming or generating devices in a home are measured by these devices. This is called cumulative measuring.

To mark a device that measures cumulative power and energy usage, set the `cumulative` property to `true` in your driver's configuration.

In case of a gas or water meter device, the `cumulative` property can also be applied. Homey will then read the `meter_gas` and `meter_water` capabilities to determine the gas and water usage of the whole home.

* The `meter_gas` capability tracks the total amount of gas consumed over time, measured in cubic metres (m3).
* The `meter_water` capability tracks the total amount of water consumed over time, measured in cubic metres (m3).

Both capabilities should be positive and continuously increase. It is typically reset only when the device is reset or reinstalled.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```json
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "class": "sensor",
  "capabilities": ["measure_power"],
  "energy": {
    "cumulative": true
  }
}
```

{% endcode %}

{% hint style="info" %}
All power-consuming devices in Homey are subtracted from the total measured power usage of all `cumulative` devices. The remaining, unaccounted-for power usage will be displayed as "other."
{% endhint %}

Most devices that track the total power usage of a home or power group are capable of measuring both imported and exported energy. For instance, a P1 meter can measure energy imported from the grid as well as energy exported back to the grid (e.g. solar-generated energy).

If a device can measure both imported and exported energy for the whole home or power group, the driver should define the `cumulativeImportedCapability` and `cumulativeExportedCapability` energy properties, as shown below.

* **Imported energy** refers to the cumulative energy imported from the device's perspective.
* **Exported energy** refers to the cumulative energy exported from the device's perspective.

These properties should be assigned to the corresponding capabilities of the device responsible for measuring imported and exported energy. If the device only supports measuring imported energy you can omit the `cumulativeExportedCapability` . If the device does not support separate measurement of imported and exported energy at all, you should omit these properties, this will result in the device being excluded from features that require the distinction between imported and exported energy.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```json
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "class": "sensor",
  "capabilities": [
    "measure_power",
    "meter_power.imported",
    "meter_power.exported"
  ],
  "capabilitiesOptions": {
    "meter_power.imported": {
      "title": { "en": "Imported Energy" }
    },
    "meter_power.exported": {
      "title": { "en": "Exported Energy" }
    }
  },
  "energy": {
    "cumulative": true,
    "cumulativeImportedCapability": "meter_power.imported",
    "cumulativeExportedCapability": "meter_power.exported"
  }
}
```

{% endcode %}

{% hint style="info" %}
The `cumulativeImportedCapability` and `cumulativeExportedCapability` properties are only used on Homey Pro (Early 2023), Homey Pro mini, and Homey Cloud and are available as of Homey v12.3.0.
{% endhint %}

### Home batteries

Home batteries are devices that can store energy for later use. When creating a driver for a home battery device, apply the device class `battery` and set the `homeBattery` property in the energy object of the driver.

#### Measure & meter power

Home batteries should have the `measure_power` capability. This represents the real-time power consumption of the home battery in Watts. Provide positive values to indicate the battery is consuming power (charging), provide negative values to indicate the battery is delivering power back to the home (discharging).

{% hint style="info" %}
The sign convention for `measure_power` matches `target_power`: positive values indicate charging (consuming power), negative values indicate discharging (producing power). This differs from solar panels where positive `measure_power` indicates generation.
{% endhint %}

In case the home battery does not support this, you can fallback to the `battery_charging_state` capability. This indicates the current state of the battery, charging, discharging or idle. By omitting the `measure_power` capability some functionality in *Energy* will be lost.

Additionally, home batteries should have the `measure_battery` capability to indicate the current state of charge of the battery.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```json
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "class": "battery",
  "capabilities": ["measure_power", "measure_battery"],
  "energy": {
    "homeBattery": true
  }
}
```

{% endcode %}

{% hint style="info" %}
The `homeBattery` energy property is available as of Homey v12.3.0.
{% endhint %}

Since home batteries both consume and produce energy, drivers should define separate `meterPowerImportedCapability` and `meterPowerExportedCapability` energy properties. This enables Homey to accurately track charged versus discharged energy. If these properties are omitted, the device will be excluded from features that require the distinction between charged and discharged energy.

* **Imported energy** refers to the energy charged by the battery.
* **Exported energy** refers to the energy discharged by the battery.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```json
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "class": "battery",
  "capabilities": [
    "measure_power",
    "measure_battery",
    "meter_power.charged",
    "meter_power.discharged"
  ],
  "capabilitiesOptions": {
    "meter_power.charged": {
      "title": { "en": "Charged Energy" }
    },
    "meter_power.discharged": {
      "title": { "en": "Discharged Energy" }
    }
  },
  "energy": {
    "homeBattery": true,
    "meterPowerImportedCapability": "meter_power.charged",
    "meterPowerExportedCapability": "meter_power.discharged"
  }
}
```

{% endcode %}

{% hint style="info" %}
The `meterPowerImportedCapability` and `meterPowerExportedCapability` are only used on Homey Pro (Early 2023), Homey Pro mini, and Homey Cloud, and are available as of v12.4.5.
{% endhint %}

#### Target power

Home batteries that support power control use `target_power` to set charging and discharging power, and optionally `target_power_mode` to determine who controls the `target_power`. Adding this capability is optional and only useful when the device has its own smart logic such as internal scheduling, self-consumption optimization, cloud control, or app-based control. Without `target_power_mode`, Homey assumes full control at all times, which is suitable for simple devices without built-in power management.

{% hint style="info" %}
Unlike EV chargers, home batteries respond immediately to `target_power` changes, there is no separate start/stop capability.
{% endhint %}

{% hint style="info" %}
If the battery has a minimum charge/discharge threshold, use the `excludeMin`/`excludeMax` capability options to define the dead zone.
{% endhint %}

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```json
{
  "name": { "en": "Home Battery" },
  "class": "battery",
  "capabilities": [
    "measure_power",
    "measure_battery",
    "meter_power.charged",
    "meter_power.discharged",
    "target_power",
    "target_power_mode"
  ],
  "capabilitiesOptions": {
    "meter_power.charged": {
      "title": { "en": "Charged Energy" }
    },
    "meter_power.discharged": {
      "title": { "en": "Discharged Energy" }
    },
    "target_power": {
      "min": -5000,
      "max": 5000
    }
  },
  "energy": {
    "homeBattery": true,
    "meterPowerImportedCapability": "meter_power.charged",
    "meterPowerExportedCapability": "meter_power.discharged"
  }
}
```

{% endcode %}

It is recommended to add the `target_power_mode` capability:

* In **device** mode, the battery uses its own optimization logic (e.g., self-consumption, time-of-use)
* In **homey** mode, Homey directly controls power flow:
  * Set to **3000** → charge at 3000W
  * Set to **0** → idle
  * Set to **−3000** → discharge at 3000W

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require("homey");

class MyHomeBattery extends Homey.Device {
  async onInit() {
    this.registerCapabilityListener("target_power", async (value) => {
      const mode = this.getCapabilityValue("target_power_mode");
      if (mode !== "homey") return;
      await this.applyTargetPower(value);
    });

    this.registerCapabilityListener("target_power_mode", async (mode) => {
      if (mode === "homey") {
        const value = this.getCapabilityValue("target_power");
        await this.applyTargetPower(value);
      } else {
        // "device" mode: let device firmware resume internal balancing
        await this.enableBuiltInScheduling();
      }
    });
  }

  async applyTargetPower(value) {
    if (value > 0) {
      await this.setDeviceChargingPower(value);
      this.log(`Charging at ${value}W`);
    } else if (value < 0) {
      await this.setDeviceDischargingPower(Math.abs(value));
      this.log(`Discharging at ${Math.abs(value)}W`);
    } else {
      await this.setDeviceIdle();
      this.log("Battery idle");
    }
  }
}

module.exports = MyHomeBattery;
```

{% endcode %}

### EV chargers

EV chargers are devices that can be used at home to charge [EVs](#evs). When creating a driver for an EV charger, apply the device class `evcharger` and set the `evCharger` property in the energy object of the driver.

#### Measure & meter power

EV chargers should have the `measure_power` capability. This represents the real-time power consumption of the EV charger in Watts. This value can be positive and negative.

* Positive when the EV charger is charging the connected EV.
* Negative when the EV charger supports bi-directional charging and is currently discharging the connected EV.

To accurately reflect the charging state of the EV charger add the `evcharger_charging_state` capability. This allows the user to easily see the state of the EV charger and act on state changes like "EV is plugged in".

Finally, to allow for easy control of the EV charger add the `evcharger_charging` capability. This capability acts like the on/off switch with regards to charging, and automatically generates useful Flow cards like "Start charging" and "Is charging".

By default the `meter_power` capability will be used to determine the energy charged by the EV charger. To enable Homey to distinguish between the charged and discharged energy of the EV charger, the driver should define the `meterPowerImportedCapability` and `meterPowerExportedCapability` energy properties, as shown below.

* **Imported energy** refers to the energy charged by the EV charger.
* **Exported energy** refers to the energy discharged by the EV charger.

These properties should be assigned to the corresponding capabilities of your device responsible for measuring charged and discharged energy. If your device does not support discharging just omit the `meterPowerExportedCapability` property.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```json
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "class": "evcharger",
  "capabilities": [
    "measure_power",
    "evcharger_charging",
    "evcharger_charging_state",
    "meter_power.charged",
    "meter_power.discharged"
  ],
  "capabilitiesOptions": {
    "meter_power.charged": {
      "title": { "en": "Charged Energy" }
    },
    "meter_power.discharged": {
      "title": { "en": "Discharged Energy" }
    }
  },
  "energy": {
    "evCharger": true,
    "meterPowerImportedCapability": "meter_power.charged",
    "meterPowerExportedCapability": "meter_power.discharged"
  }
}
```

{% endcode %}

{% hint style="info" %}
The `evCharger` property and the `evcharger_charging` and `evcharger_charging_state` capabilities are available as of v12.4.5.
{% endhint %}

#### Target power

EV chargers that support power control should use `target_power` to set the charging/discharging power and optionally `target_power_mode` to determine who controls the `target_power`. Adding this capability is optional and only useful when the device has its own smart logic such as internal scheduling, self-consumption optimization, cloud control, or app-based control. Without `target_power_mode`, Homey assumes full control at all times, which is suitable for simple devices without built-in power management.

{% hint style="info" %}
**Auto start/stop charging:** When using the **Set target power** Flow card Homey automatically:

* Sets `target_power_mode` to `homey`
* Sets `evcharger_charging` to `true` (positive value) or `false` (zero/negative)
  {% endhint %}

{% hint style="warning" %}
**Configure the widest possible min/max range**

Set `min` and `max` to represent the **full operating range** across all configurations. Homey uses these values to determine what power levels it can request.

For EV chargers that support multiple phase configurations (1/2/3-phase), use the widest range: 1-phase minimum to 3-phase maximum. The driver must handle phase switching internally.

```json
"capabilitiesOptions": {
  "target_power": {
    "min": 0,         // allows idle (0W)
    "max": 22000,     // 3-phase maximum
    "step": 230,      // finest step size
    "excludeMin": 0,
    "excludeMax": 1380  // 1-phase minimum (6A × 230V)
  }
}
```

You can use `setCapabilityOptions()` to update values dynamically, but don't do this often as it is an expensive call.
{% endhint %}

{% hint style="info" %}
**Use exclude range for minimum charging power:** Most EVs cannot charge below 6A (\~1380W single-phase). Use `excludeMin`/`excludeMax` to define this dead zone. Values inside the exclude range automatically become 0 (idle).
{% endhint %}

**Example: Unidirectional EV charger**

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```json
{
  "name": { "en": "EV Charger" },
  "class": "evcharger",
  "capabilities": [
    "measure_power",
    "evcharger_charging",
    "evcharger_charging_state",
    "meter_power",
    "target_power",
    "target_power_mode"
  ],
  "capabilitiesOptions": {
    "target_power": {
      "min": 0,
      "max": 22000,
      "step": 230,
      "excludeMin": 0,
      "excludeMax": 1380
    }
  },
  "energy": {
    "evCharger": true
  }
}
```

{% endcode %}

**Example: Bidirectional EV charger**

Bidirectional chargers use negative values for discharging.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```json
{
  "name": { "en": "EV Charger" },
  "class": "evcharger",
  "capabilities": [
    "measure_power",
    "evcharger_charging",
    "evcharger_charging_state",
    "meter_power.charged",
    "meter_power.discharged",
    "target_power",
    "target_power_mode"
  ],
  "capabilitiesOptions": {
    "meter_power.charged": {
      "title": { "en": "Charged Energy" }
    },
    "meter_power.discharged": {
      "title": { "en": "Discharged Energy" }
    },
    "target_power": {
      "min": -11000,
      "max": 22000,
      "step": 230,
      "excludeMin": -1380,
      "excludeMax": 1380
    }
  },
  "energy": {
    "evCharger": true,
    "meterPowerImportedCapability": "meter_power.charged",
    "meterPowerExportedCapability": "meter_power.discharged"
  }
}
```

{% endcode %}

**Configuration**

The `excludeMin`/`excludeMax` range handles the minimum operating threshold (6A × 230V = 1380W), any value between 1–1379W automatically becomes 0. `step: 230` corresponds to 1A at 230V, the finest granularity for single-phase charging.

* In **device** mode, the charger uses its own built-in charging logic
* In **homey** mode, Homey controls the charging rate via `target_power`. Use `evcharger_charging` to start or stop charging
* When charging stops, the `target_power` value is preserved for the next session

**Example: Capability value processing**

| Requested | Result              | Reason                               |
| --------- | ------------------- | ------------------------------------ |
| 5000W     | Charge at 4830W     | Rounded down to nearest step (230W)  |
| 1000W     | Idle (0W)           | Inside exclude range (−1380 to 1380) |
| −1000W    | Idle (0W)           | Inside exclude range                 |
| −5000W    | Discharge at 4830W  | Rounded toward zero to nearest step  |
| 25000W    | Charge at 22000W    | Clamped to max (22000)               |
| −15000W   | Discharge at 11000W | Clamped to min (−11000)              |

**Example: Capability listeners**

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require("homey");

class MyEVCharger extends Homey.Device {
  async onInit() {
    this.registerMultipleCapabilityListener(
      ["target_power", "target_power_mode", "evcharger_charging"],
      async ({ target_power, target_power_mode, evcharger_charging }) => {
        // All capability changes arrive as a single debounced batch.
        // Only the changed capabilities are present in the object.

        // Handle mode switch
        if (target_power_mode === "device") {
          await this.enableBuiltInScheduling();
          this.log("Switched to device mode");
          return;
        }

        // Handle stop charging
        if (evcharger_charging === false) {
          await this.stopCharging();
          this.log("Stopped charging/discharging");
          return;
        }

        // Handle start charging / power adjustment
        const isCharging =
          evcharger_charging === true ||
          this.getCapabilityValue("evcharger_charging");
        const power =
          target_power ?? this.getCapabilityValue("target_power") ?? 0;

        if (evcharger_charging === true) {
          // Start charging at the current target power
          const amps = Math.round(Math.abs(power) / 230);
          if (power >= 0) {
            await this.startCharging(amps);
            this.log(`Started charging at ${power}W (${amps}A)`);
          } else {
            await this.startDischarging(amps);
            this.log(`Started discharging at ${Math.abs(power)}W (${amps}A)`);
          }
        } else if (target_power != null && isCharging) {
          // Adjust charging rate while already charging
          const amps = Math.round(Math.abs(target_power) / 230);
          if (target_power > 0) {
            await this.setChargingCurrent(amps);
            this.log(`Charging power adjusted to ${target_power}W (${amps}A)`);
          } else if (target_power < 0) {
            await this.setDischargingCurrent(amps);
            this.log(
              `Discharging power adjusted to ${Math.abs(target_power)}W (${amps}A)`,
            );
          } else {
            await this.setChargingCurrent(0);
            this.log("Power set to idle (0W)");
          }
        } else if (target_power !== undefined && !isCharging) {
          this.log(
            `Target power set to ${target_power}W, will apply when charging starts`,
          );
        }
      },
      500, // debounce timeout in ms
    );
  }
}

module.exports = MyEVCharger;
```

{% endcode %}

{% hint style="info" %}
**Why `registerMultipleCapabilityListener`?** When a Flow sets `target_power`, Homey also sets `target_power_mode` and `evcharger_charging` in quick succession. `registerMultipleCapabilityListener` debounces these into a single callback, reducing API calls to the physical device. This is especially important when `target_power` is updated frequently by a Flow.

You can also use separate `registerCapabilityListener` calls per capability. This is simpler but means the driver receives each change individually, potentially sending multiple API calls to the device for what is logically a single operation.
{% endhint %}

**Example: Dynamic phase configuration**

If your EV charger switches between phase configurations (e.g., automatically or via a setting), you can update the capability options dynamically. But, use this sparingly as `setCapabilityOptions()` is an expensive operation.

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
async onPhaseConfigChanged(phaseMode) {
  // phaseMode: 1, 2, or 3
  const voltage = 230;
  const minAmps = 6;
  const minPower = minAmps * phaseMode * voltage;

  await this.setCapabilityOptions("target_power", {
    min: -32 * phaseMode * voltage,     // Max discharge
    max: 32 * phaseMode * voltage,      // Max charge (32A per phase)
    step: phaseMode * voltage,          // 230W, 460W, or 690W
    excludeMin: -minPower,              // -1380W, -2760W, or -4140W
    excludeMax: minPower,               // 1380W, 2760W, or 4140W
  });

  this.log(`Phase configuration changed to ${phaseMode}-phase`);
}
```

{% endcode %}

{% hint style="info" %}
**Range must include zero:** The `min`/`max` range of `target_power` must always include 0 (`min <= 0 <= max`). All devices need the ability to idle. If your device has a minimum operating threshold, use the `excludeMin`/`excludeMax` capability options instead of setting `min` above zero.
{% endhint %}

{% hint style="info" %}
**Exclude range validation:** The exclude range must include 0 (`excludeMin <= 0 <= excludeMax`). Values exactly at the boundaries are valid; only values strictly between them become 0.
{% endhint %}

### EVs

EVs are battery electric cars. These cars can be charged by an EV charger. When creating a driver for an EV, apply the device class `car` and set the `electricCar` property in the energy object of the driver.

EVs should have the `measure_battery` capability which represents the current state of charge of the battery. Additionally, the `ev_charging_state` capability should be added when the EV can report its current charging state (plugged in/out, charging, discharging).

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```json
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "class": "car",
  "capabilities": ["measure_battery", "ev_charging_state"],
  "energy": {
    "electricCar": true
  }
}
```

{% endcode %}

{% hint style="info" %}
The `electricCar` energy property and `ev_charging_state` capability are available as of Homey v12.4.5.
{% endhint %}

## Batteries

All devices with the `measure_battery` or `alarm_battery` capability (except home batteries and EVs) must specify which type and the amount of batteries they use. This will be shown to the user in the UI.

For example, a device with 2x AAA batteries:

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```json
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "class": "thermostat",
  "capabilities": [
    "measure_battery",
    "measure_temperature",
    "target_temperature"
  ],
  "energy": {
    "batteries": ["AAA", "AAA"]
  }
}
```

{% endcode %}

Possible battery values are:

* `LS14250`
* `C`
* `AA`
* `AAA`
* `AAAA`
* `A23`
* `A27`
* `PP3`
* `CR123A`
* `CR2`
* `CR1632`
* `CR2032`
* `CR2430`
* `CR2450`
* `CR2477`
* `CR3032`
* `CR14250`
* `INTERNAL`
* `OTHER`

## Settings

Homey provides a couple of settings to Energy devices under certain conditions:

### Always on

This setting is provided to devices that have device class `socket` and the `onoff` capability. By default it is disabled, but when enabled, Homey will prevent the user from turning off the device through Homey. An error message will be returned when this is attempted.

### Exclude from Energy

This setting is provided to devices that have the `meter_power` or `measure_power` capability, have device class `solarpanel`(or *Solar panel* in the *What's plugged in?* setting), or are marked as `cumulative` measuring devices (see [#cumulative-measuring-devices](#cumulative-measuring-devices "mention")). By default it is disabled, but when enabled Homey will no longer show the device in the Zone Control Energy, nor will it be included in future Energy reports (note: this will not remove the device from existing Energy reports).

### Tracks total home energy consumption

This setting is provided to devices that are marked as `cumulative` measuring devices (see [#cumulative-measuring-devices](#cumulative-measuring-devices "mention")). By default this setting is enabled, this will make sure Homey considers this device as a device that measures energy consumption at the highest level in the home. When disabled it will consider the device as a regular device in the home that measures energy consumption.

### Invert power measurement

This setting is available for devices with the `socket` device class when the user selects "Solar panel" in the *What's plugged in?* setting. It allows the user to invert the sign (positive or negative) of the `measure_power` capability for this device. This is useful for accurately representing power flow, ensuring that produced power is displayed correctly. For more details, see [Smart plugs](#smart-plugs).

### Power usage when off

This setting is provided to devices that do *not* have the `measure_power` capability and *do* have the `onoff` capability. The default value for this setting can be determined by the driver (see [#constant-power-usage](#constant-power-usage "mention")). It will be used to approximate the device's power consumption.

### Power usage when on

This setting is provided to devices that do *not* have the `measure_power` capability and *do* have the `onoff` capability. The default value for this setting can be determined by the driver (see [#constant-power-usage](#constant-power-usage "mention")). It will be used to approximate the device's power consumption.

### Constant power usage

This setting is provided to devices that do *not* have the `measure_power`, `onoff`, `measure_battery` or `alarm_battery` capability. Devices that have defined `batteries` in the driver's manifest will also not get this setting, nor will devices with a device class listed below. The default value for this setting can be determined by the driver (see [#constant-power-usage](#constant-power-usage "mention")). It will be used to approximate the device's power consumption.

* `button`
* `windowcoverings`
* `blinds`
* `curtain`
* `sunshade`
* `kettle`
* `coffeemachine`
* `remote`
* `solarpanel`
* `vacuumcleaner`
* `thermostat`


# Settings

Device settings allow users to customize the behaviour of their devices from Homey.

Devices can have settings that can be changed by the user. These are presented to the user as *Advanced settings*. The device settings are defined in the `/drivers/<driver_id>/driver.settings.compose.json` file.

<div align="center"><figure><img src="/files/ne1QqKp7c961R6cRs7VQ" alt="" width="375"><figcaption></figcaption></figure></div>

{% code title="/drivers/\<driver\_id>/driver.settings.compose.json" %}

```javascript
[
  {
    "id": "username",
    "type": "text",
    "label": { "en": "Username" },
    "value": "John Doe",
    "hint": { "en": "The name of the user." }
  },
  {
    "id": "password",
    "type": "password",
    "label": { "en": "Password" },
    "value": "Secret",
    "hint": { "en": "The password of the user." }
  }
]
```

{% endcode %}

## Defining settings

Every setting has a `type` property that determines what values it can have and how it is presented to the user. The `value` property of each setting is the initial value of the setting, this property is required for all settings. The following setting types are supported:

### Text

This is a single line text input whose value is a `string`. You can optionally validate the value using a regex pattern by adding a `pattern` property. For example adding `"pattern": "[a-zA-Z]"` to the setting definition will make sure the user can only input letters.

```javascript
{
  "id": "username",
  "type": "text",
  "label": { "en": "Username" },
  "value": "John Doe",
  "hint": { "en": "The name of the user." }
}
```

### Password

Settings with the type `password` behave the same as `text` but the input is visually hidden.

```javascript
{
  "id": "password",
  "type": "password",
  "label": { "en": "Password" },
  "value": "Secret",
  "hint": { "en": "The password of the user." }
}
```

### Text area

`textarea` type settings behave the same as `text` but allow multi-line input.

```javascript
{
  "id": "description",
  "type": "textarea",
  "label": { "en": "Description" },
  "value": "Initial description",
  "hint": { "en": "A custom device description." }
}
```

### Number

The `number` type settings can only contain numbers, this means that the `value` must also be a number. Number settings also support `min` `max` and `step` properties. You can also optionally provide the unit of the setting by setting the `units` property.

```javascript
{
  "id": "duration",
  "type": "number",
  "label": { "en": "Duration" },
  "value": 3,
  "min": 0,
  "max": 5,
  "units": { "en": "minutes" }
}
```

### Checkbox

`checkbox` settings can be `true` or `false`.

```javascript
{
  "id": "allow_override",
  "type": "checkbox",
  "value": true,
  "label": { "en": "Allow override" }
}
```

### Dropdown

Settings with the type `dropdown` allow the user to pick a value from a predefined set of choices. The values of the checkbox must be strings.

```javascript
{
  "id": "mode",
  "type": "dropdown",
  "value": "heating",
  "label": { "en": "Default mode" },
  "values": [
    {
      "id": "heating",
      "label": { "en": "Heating" }
    },
    {
      "id": "cooling",
      "label": { "en": "Cooling" }
    }
  ]
}
```

### Group

You can add a `type` group to your settings to group multiple settings together with a label.

```javascript
{
  "type": "group",
  "label": { "en": "Login details" },
  "children": [
    {
      "id": "username",
      "type": "text",
      "label": { "en": "Username" },
      "value": "John Doe",
      "hint": { "en": "The name of the user." }
    },
    {
      "id": "password",
      "type": "password",
      "label": { "en": "Password" },
      "value": "Secret",
      "hint": { "en": "The password of the user." }
    }
  ]
}
```

### Label

You can add additional explanation or headings to the device settings page by adding a setting with the type `label`. This acts as a read-only text field and can only be updated by your app.

```javascript
{
  "id": "label",
  "type": "label",
  "label": { "en": "IP address" },
  "value": "192.168.0.10",
  "hint": { "en": "The IP address of the device." }
}
```

## Highlighted settings

<figure><img src="/files/yico0LthIFscHMnOuqQx" alt="" width="375"><figcaption><p>Highlighted settings for a camera device.</p></figcaption></figure>

In an app with an extensive list of settings, it can be challenging for users to locate the most important ones. To address this, there's an option to display key settings in a separate list during the pairing of a new device. This feature, called "Highlighted Settings," helps users quickly find the most essential settings.

To highlight a settings item, simply add `"highlight": true` to the item in your settings JSON file.

{% hint style="warning" %}
Be selective when choosing highlighted settings. The goal is to feature a few frequently used items. Highlighting too many settings can make the highlighted list just as difficult to navigate as the full list of settings.
{% endhint %}

## Using settings

{% tabs fullWidth="false" %}
{% tab title="JavaScript" %}
Once you have defined what settings your device has you can read the settings in from your `Device` class. You can retrieve the current values of all settings as follows:

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require('homey');

class Device extends Homey.Device {
  async onInit() {
    const settings = this.getSettings();
    console.log(settings.username);
  }
}

module.exports = Device;
```

{% endcode %}

When settings are changed by a user, [`Device#onSettings()`](https://apps-sdk-v3.developer.homey.app/Device.html#onSettings) will be called. You can overwrite the method in your `device.js` to react to changes to the settings. It is also possible to throw an error from this method if the settings are invalid. The thrown error will be shown to the user and they be asked to change their settings in order to store them.

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require('homey');

class Device extends Homey.Device {
  async onSettings({ oldSettings, newSettings, changedKeys }) {
    // run when the user has changed the device's settings in Homey.
    // changedKeysArr contains an array of keys that have been changed
    // if the settings must not be saved for whatever reason:
    // throw new Error('Your error message');
  }
}

module.exports = Device;
```

{% endcode %}

It is also possible to update the settings from your `Device` by calling [`Device#setSetting()`](https://apps-sdk-v3.developer.homey.app/Device.html#setSetting).

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require('homey');

class Device extends Homey.Device {
  async onInit() {
    await this.setSettings({
      // only provide the settings you want to change
      username: "Jane Doe",
    });
  }
}

module.exports = Device;
```

{% endcode %}

{% hint style="info" %}
When changing device settings programmatically using [`Device#setSettings()`](https://apps-sdk-v3.developer.homey.app/Device.html#setSettings), the [`Device#onSettings()`](https://apps-sdk-v3.developer.homey.app/Device.html#onSettings) function is not fired.
{% endhint %}
{% endtab %}

{% tab title="TypeScript" %}
Once you have defined what settings your device has you can read the settings in from your `Device` class. You can retrieve the current values of all settings as follows:

{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey from "homey";

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    const settings = this.getSettings();
    console.log(settings.username);
  }
}

```

{% endcode %}

When settings are changed by a user, [`Device#onSettings()`](https://apps-sdk-v3.developer.homey.app/Device.html#onSettings) will be called. You can overwrite the method in your `device.js` to react to changes to the settings. It is also possible to throw an error from this method if the settings are invalid. The thrown error will be shown to the user and they be asked to change their settings in order to store them.

{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey from "homey";

type SettingsEvent<T> = {
  oldSettings: T;
  newSettings: T;
  changedKeys: (keyof T)[];
};

type Settings = {
  username: string;
};

export default class Device extends Homey.Device {
  async onSettings({ oldSettings, newSettings, changedKeys }: SettingsEvent<Settings>): Promise<string | void> {
    // runs when the user has changed the device's settings in Homey.
    // changedKeys contains the keys that have been changed
    // if the settings must not be saved for whatever reason:
    // throw new Error('Your error message');
  }
}

```

{% endcode %}

It is also possible to update the settings from your `Device` by calling [`Device#setSetting()`](https://apps-sdk-v3.developer.homey.app/Device.html#setSetting).

{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey from "homey";

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    await this.setSettings({
      // only provide the settings you want to change
      username: "Jane Doe",
    });
  }
}

```

{% endcode %}

{% hint style="info" %}
When changing device settings programmatically using [`Device#setSettings()`](https://apps-sdk-v3.developer.homey.app/Device.html#setSettings), the [`Device#onSettings()`](https://apps-sdk-v3.developer.homey.app/Device.html#onSettings) function is not fired.
{% endhint %}
{% endtab %}

{% tab title="Python" %}
Once you have defined what settings your device has you can read the settings in from your `Device` class. You can retrieve the current values of all settings as follows:

{% code title="/drivers/\<driver\_id>/device.mts" %}

```python
from homey import device


class Device(device.Device):
    async def on_init(self) -> None:
        settings = self.get_settings()
        print(settings.get("username"))


homey_export = Device

```

{% endcode %}

When settings are changed by a user, [`Device#on_settings()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.on_settings) will be called. You can overwrite the method in your `device.js` to react to changes to the settings. It is also possible to throw an error from this method if the settings are invalid. The thrown error will be shown to the user and they be asked to change their settings in order to store them.

{% code title="/drivers/\<driver\_id>/device.py" %}

```python
from homey import device


class Device(device.Device):
    async def on_settings(
        self,
        old_settings,
        new_settings,
        changed_keys,
    ):
        # runs when the user has changed the device's settings in Homey.
        # changed_keys contains the keys of settings that have been changed
        # if the settings must not be saved for whatever reason:
        # raise Exception("Your error message")
        ...


homey_export = Device

```

{% endcode %}

It is also possible to update the settings from your `Device` by calling [`Device#set_setting()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.set_settings).

{% code title="/drivers/\<driver\_id>/device.py" %}

```python
from homey import device


class Device(device.Device):
    async def on_init(self) -> None:
        await self.set_settings(
            {
                # only provide the settings you want to change
                "username": "Jane Doe",
            }
        )


homey_export = Device

```

{% endcode %}

{% hint style="info" %}
When changing device settings programmatically using [`Device#set_settings()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.set_settings), the [`Device#on_settings()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.on_settings) function is not fired.
{% endhint %}
{% endtab %}
{% endtabs %}

## Reserved Settings

The following prefixes are **reserved** by Homey and **must not** be used as the beginning of your setting `id`:

* `homey:`
* `zw_`
* `zb_`
* `mtr_`
* `thread_`
* `zone_`
* `energy_`
* `satellite_mode_`
* `homekit_`


# Best practices

Device best practices that your app should follow.

Users expect all their devices to behave in a consistent way. However since all devices are different and may be implemented by different apps created by different developers it is important to know what the desired behaviour is. For most capabilities and device classes this is straightforward, however sometimes there are some important details that apps need to get right in order to have a pleasant user experience.

The following guides will explain how certain device classes and capabilities should be implemented in order to offer a consistent user experience across different devices.

{% content-ref url="/pages/-MWOUD86EGo6yleSW\_27" %}
[Lights](/the-basics/devices/best-practices/lights)
{% endcontent-ref %}

{% content-ref url="/pages/-MWOUI2mHUD4IFz28p2Q" %}
[Window coverings](/the-basics/devices/best-practices/window-coverings)
{% endcontent-ref %}

{% content-ref url="/pages/-MWORc69bQbhxQ75DF1A" %}
[Battery status](/the-basics/devices/best-practices/battery-status)
{% endcontent-ref %}


# Lights

Best practices for 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.

## 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.

## Capabilities and behaviour

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

### 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`).

![](/files/-MY9hDARrcX0ZL6GM8e6)

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.

{% tabs %}
{% tab title="JavaScript" %}
The `onoff` and `dim` capabilities should be coupled together and debounced. This can be done using the [`Device#registerMultipleCapabilityListener()`](https://apps-sdk-v3.developer.homey.app/Device.html#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.

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require('homey');
const DeviceApi = require('device-api');

class Device extends Homey.Device {
  async onInit() {
    this.registerMultipleCapabilityListener(['onoff', 'dim'], async ({ onoff, dim }) => {
      if (dim > 0 && onoff === false) {
        await DeviceApi.setOnOffAsync(false); // turn off
      } else if (dim <= 0 && onoff === true) {
        await DeviceApi.setOnOffAsync(true); // turn on
      } else {
        await DeviceApi.setOnOffAndDimAsync({ onoff, dim }); // turn on or off and set dim level in one command
      }
    });
  }
}

module.exports = Device;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
The `onoff` and `dim` capabilities should be coupled together and debounced. This can be done using the [`Device#registerMultipleCapabilityListener()`](https://apps-sdk-v3.developer.homey.app/Device.html#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.

{% code title="/drivers/\<driver\_id>/device.mts" %}

```javascript
import Homey from "homey";
import DeviceApi from "./device-api.mjs";

type LightCapabilityValues = { onoff: boolean; dim: number };

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    this.registerMultipleCapabilityListener(["onoff", "dim"], async ({ onoff, dim }: LightCapabilityValues) => {
      if (dim > 0 && !onoff) {
        await DeviceApi.setOnOffAsync(false); // turn off
      } else if (dim <= 0 && onoff) {
        await DeviceApi.setOnOffAsync(true); // turn on
      } else {
        await DeviceApi.setOnOffAndDimAsync({ onoff, dim }); // turn on or off and set dim level in one command
      }
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
The `onoff` and `dim` capabilities should be coupled together and debounced. This can be done using the [`Device#register_multiple_capability_listener()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.register_multiple_capability_listener) 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.

{% code title="/drivers/\<driver\_id>/device.py" %}

```python
from typing import TypedDict

from device_api import DeviceApi
from homey import device


class LightCapabilityValues(TypedDict, extra_items=device.CapabilityValue):
    onoff: bool
    dim: float


class Device(device.Device):
    async def on_init(self) -> None:
        async def light_capability_listener(
            values: LightCapabilityValues, **kwargs
        ) -> None:
            onoff, dim = values["onoff"], values["dim"]
            if dim > 0 and not onoff:
                await DeviceApi.set_on_off_async(False)  # turn off
            elif dim <= 0 and onoff:
                await DeviceApi.set_on_off_async(True)  # turn on
            else:
                await DeviceApi.set_on_off_and_dim_async(
                    **values
                )  # turn on or off and set dim level in one command

        self.register_multiple_capability_listener(
            ["onoff", "dim"], light_capability_listener
        )


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

{% hint style="info" %}
Using the `setOnDim` [capability option](/the-basics/devices/capabilities#capability-options) for the `onoff` capability will result in Homey sending only a `dim` capability set instead of both `onoff` and `dim` when a Flow changes the dim level of a device.
{% endhint %}

### 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.

![](/files/-MYdjx6naljBbmWFdm7J) ![](/files/-MY9hDATsMrFsLp5Kdv2)

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.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require('homey');
const DeviceApi = require('device-api');

const lightCapabilities = [
  "onoff",
  "dim",
  "light_hue",
  "light_temperature",
  "light_saturation",
  "light_mode",
];

class Device extends Homey.Device {
  async onInit() {
    this.registerMultipleCapabilityListener(
      lightCapabilities,
      async ({
        onoff,
        dim,
        light_hue,
        light_temperature,
        light_saturation,
        light_mode
      }) => {
        // handle the changed capabilities all at once
        await DeviceApi.setOnOffAndDimAndColorAsync();
      }
    );
  }
}

module.exports = Device;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey from "homey";
import DeviceApi from "./device-api.mjs";

type LightCapabilityValues = {
  onoff: boolean;
  dim: number;
  light_hue: number;
  light_temperature: number;
  light_saturation: number;
  light_mode: "color" | "temperature";
};

const lightCapabilities = [
  "onoff",
  "dim",
  "light_hue",
  "light_temperature",
  "light_saturation",
  "light_mode"
];

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    this.registerMultipleCapabilityListener(lightCapabilities, async (values: LightCapabilityValues) => {
      // Handle the changed capabilities all at once
      await DeviceApi.setOnOffAndDimAndColorAsync(values);
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/drivers/\<driver\_id>/device.py" %}

```python
from typing import Literal, TypedDict

from device_api import DeviceApi
from homey import device


class LightCapabilityValues(TypedDict, extra_items=device.CapabilityValue):
    onoff: bool
    dim: float
    light_hue: float
    light_temperature: float
    light_saturation: float
    light_mode: Literal["color", "temperature"]


light_capabilities = [
    "onoff",
    "dim",
    "light_hue",
    "light_temperature",
    "light_saturation",
    "light_mode",
]


class Device(device.Device):
    async def on_init(self) -> None:
        async def light_capability_listener(
            values: LightCapabilityValues, **kwargs
        ) -> None:
            # Handle the changed capabilities all at once
            await DeviceApi.set_on_off_and_dim_and_color_async(values)

        self.register_multiple_capability_listener(
            light_capabilities, light_capability_listener
        )


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}


# Window coverings

Window coverings device best practices.

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:

**window coverings that can be controlled with up/down and stop commands**

This type should 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.

**window coverings that can be controlled by sending a command with a precise open/close level**

This type should 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.

**window coverings that can be controlled by sending a command with precise open/close level and up, down and stop commands**

This type should implement both `windowcoverings_state` and `windowcoverings_set`.


# Battery status

Best practices for devices with batteries.

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:

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png"
  },
  "class": "sensor",
  "capabilities": ["measure_battery"],
  "energy": {
    "batteries": ["AAA", "AAA"]
  }
}
```

{% endcode %}

For more information check the [Energy documentation](/the-basics/devices/energy).


# Flow

Flow cards allow users to create advanced home automations using your the devices from your app.

{% embed url="<https://www.youtube.com/watch?v=W01YXz5HlR0>" %}

With Homey Flow, Homey users can automate their home. A Flow is a series of *Flow cards*, which are evaluated and executed.

As a developer, you can add new functionality from your app to Flow by exposing various cards.

![](/files/-MZxP9WdE7HG7r-IdhYw)

A Flow consists of cards in three columns: *when*, *and*, *then*.

* Cards in the *when...* column are called `triggers`. Your app tells Homey to fire a trigger, which will then run all of the user's flows with this trigger.
* Cards in the *...and...* column are called `conditions`. The conditions must be met in order for the flow to continue. For example *it is raining*, or *the vacuum cleaner is cleaning*.
* Cards in the *...then* column are called `actions`. Actions are executed when the trigger has been fired, and all conditions are met.

Your app can expose any of these three card types, by defining them in your App Manifest.

## Defining Flow cards

All Flow cards must at least have an `id` and a `title` property. The `id` is used to refer to the Flow card from your source code. The `title` will be shown to the users and should be a short and clear description of what the Flow card does. A Flow card can have an optional `hint` property that can be used to pass additional information to the user that can not be included in the title.

{% hint style="info" %}
Often it is not necessary to define your own Flow cards because most device classes and capabilities automatically add their own Flow cards.
{% endhint %}

{% code title="/.homeycompose/flow/triggers/rain\_start.json" %}

```javascript
{
  "title": {
    "en": "It starts raining"
  },
  "hint": {
    "en": "When it starts raining more than 0.1 mm/h."
  }
}
```

{% endcode %}

{% code title="/.homeycompose/flow/actions/stop\_raining.json" %}

```javascript
{
  "title": {
    "en": "Make it stop raining.",
  }
}
```

{% endcode %}

### Title for Flow card with arguments

When a Flow card contains arguments it may be necessary to change the title according to these arguments. Using the `titleFormatted` property it is possible to integrate argument values into the title of the Flow card.

{% code title="/.homeycompose/flow/conditions/raining\_in.json" %}

```javascript
{
  "title": 
    "en": "It !{{is|isn't}} going to rain in...",
  },
  "titleFormatted": {
    "en": "It !{{is|isn't}} going to rain in [[when]]",
  },
  "hint": {
    "en": "Checks if it will/will not rain more than 0.1 mm/h within the given amount of time.",
  },
  "args": [
    {
      "name": "when",
      "type": "dropdown",
      "values": [
        { "id": "5", "label": { "en": "5 minutes" } },
        { "id": "10", "label": { "en": "10 minutes" } },
        { "id": "15", "label": { "en": "15 minutes" } }
      ]
    }
  ]
}
```

{% endcode %}

### Title for Flow card condition

For conditional Flow cards it is possible to change the title if the Flow card is inverted. This can be done using the `!{{...|...}}` syntax. In the place of the first three dots (`...`) should be the text that will be shown if the Flow card is not inverted, in place of the second three dots should be the text that will be shown when the Flow card is inverted.

{% code title="/.homeycompose/flow/conditions/is\_raining.json" %}

```javascript
{
  "title": {
    "en": "It !{{is|isn't}} raining"
  },
  "hint": {
    "en": "Checks if it is currently raining more than 0.1 mm/h."
  }
}
```

{% endcode %}

![Example of a Flow condition card and its inverted variant.](/files/-MZxDYI5GgE9OEAH3iiO)

## Triggering a Flow

To fire a trigger, run the following code from anywhere in your app:

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/app.js" %}

```javascript
const Homey = require('homey');
const RainApi = require('rain-api');

class App extends Homey.App {
  async onInit() {
    const rainStartTrigger = this.homey.flow.getTriggerCard('rain_start');

    RainApi.on('raining', () => {
      await rainStartTrigger.trigger();
    });
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/app.mts" %}

```mts
import Homey from 'homey';
import RainApi from 'rain-api';

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    const rainStartTrigger = this.homey.flow.getTriggerCard('rain_start');

    RainApi.on('raining', async () => {
      await rainStartTrigger.trigger();
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/app.py" %}

```python
from homey import app

from rain_api import RainApi


class App(app.App):
    async def on_init(self) -> None:
        rain_start_trigger_card = self.homey.flow.get_trigger_card("rain_start")

        async def on_raining():
            await rain_start_trigger_card.trigger()

        RainApi.on("raining", on_raining)


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}

All of the Flows on the Homey, with the trigger `rain_start` will now fire. More advanced triggers can be achieved using [Tokens](/the-basics/flow/tokens#local-tokens), [State](/the-basics/flow/arguments#flow-state) and [Devices](#device-cards).

### Custom capability changed

As a convenience Homey will automatically run Flow trigger cards with specific IDs when you call `Device#setCapabilityValue()` for a custom capability.

For custom capabilities with the type "number", "enum" or "string" Homey will automatically run Flow triggers with ID `<capability_id>_changed`. When your custom capability has the type "boolean" Homey will automatically run Flow triggers with IDs `<capability_id>_true` and `<capability_id>_false` depending on the value.

For example if your capability is called `measure_clicks` Homey will automatically run Flow triggers with the ID `measure_clicks_changed`. This Flow card can have a [token](/the-basics/flow/tokens) with the same name as the capability that will be set to the current capability value.

{% code title="/drivers/\<driver\_id>/driver.flow\.compose.json" %}

```javascript
{
  "triggers": [
    {
      "id": "measure_clicks_changed",
      "title": { "en": "Clicks updated" },
      "tokens": [
          {
            "name": "measure_clicks",
            "type": "number",
            "title": { "en": "clicks" },
            "example": { "en": "Clicks" }
          }
        ],
    }
  ]
}
```

{% endcode %}

This also works for [sub-capabilities](/the-basics/devices/capabilities#using-the-same-capability-more-than-once). For example when you update the capability `measure_clicks.inside` Homey will run Flow triggers with ID `measure_clicks.inside_changed`.

## Listening for events

For every Flow card your app includes, you should register a "run" listener. Such a listener gets called when a Flow containing the card is activated. Conditions and Actions need a listener to function, however the listener is only necessary for a Flow trigger when it has one or more [Arguments](/the-basics/flow/arguments).

Condition cards must resolve with a `true` value for the Flow to continue, or a `false` value to stop the Flow from executing. When the card is rejected (e.g. by throwing inside a Promise), the Flow stops executing as well.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/app.js" %}

```javascript
const Homey = require('homey');
const RainApi = require('rain-api');

class App extends Homey.App {
  async onInit() {
    const rainingCondition = this.homey.flow.getConditionCard('is_raining');
    rainingCondition.registerRunListener(async (args, state) => {
      const raining = await RainApi.isItRaining(); // true or false
      return raining;
    });

    const stopRainingAction = this.homey.flow.getActionCard('stop_raining');
    stopRainingAction.registerRunListener(async (args, state) => {
      await RainApi.makeItStopRaining();
    });
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/app.mts" %}

```mts
import Homey from 'homey';
import RainApi from 'rain-api';

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    const rainingCondition = this.homey.flow.getConditionCard('is_raining');
    rainingCondition.registerRunListener(async (args, state) => {
      const raining: boolean = await RainApi.isItRaining();
      return raining;
    });

    const stopRainingAction = this.homey.flow.getActionCard('stop_raining');
    stopRainingAction.registerRunListener(async (args, state) => {
      await RainApi.makeItStopRaining();
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/app.py" %}

```python
from homey import app

from rain_api import RainApi


class App(app.App):
    async def on_init(self) -> None:
        async def on_raining_condition(card_arguments, **trigger_kwargs) -> bool:
            return await RainApi.is_it_raining()

        raining_condition = self.homey.flow.get_condition_card("is_raining")
        raining_condition.register_run_listener(on_raining_condition)

        async def on_stop_raining_action(card_arguments, **trigger_kwargs) -> None:
            await RainApi.make_it_stop_raining()

        stop_raining_action = self.homey.flow.get_action_card("stop_raining")
        stop_raining_action.register_run_listener(on_stop_raining_action)


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}

## Device cards

Often you will want to add Flow cards that operate on a specific device. For example you want to trigger a Flow when a button is pressed on a specific switch, not when a button is pressed on any of the switches you own. We call these types of Flow cards "device cards". These Flow cards are displayed to users as belonging to a specific device as opposed to belonging to the App.

If your driver has a `class` that Homey already has support for, like `light` or `socket`, your custom card will be added to those cards. For example, if your driver supports a light bulb that has a special disco mode, the user will see "Turn on", "Turn off", "Dim", "Set color", "Disco mode".

Device cards are defined in `/drivers/<driver_id>/driver.flow.compose.json`. Flow cards defined in this file are only shown for devices belonging to that specific driver.

{% code title="/drivers/\<driver\_id>/driver.flow\.compose.json" %}

```javascript
{
  "actions": [
    {
      "id": "disco_mode",
      "title": { "en": "Disco mode" }
    }
  ]
}
```

{% endcode %}

### Flow Device Trigger cards

When creating a Flow trigger card that should only be activated by a specific device you need to use [`ManagerFlow#getDeviceTriggerCard()`](https://apps-sdk-v3.developer.homey.app/ManagerFlow.html#getDeviceTriggerCard) instead of [`ManagerFlow#getTriggerCard()`](https://apps-sdk-v3.developer.homey.app/ManagerFlow.html#getTriggerCard). A Flow device trigger card allows you to pass the `device` to the `trigger()` method so Homey knows to only start the Flows for that specific device.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/drivers/\<driver\_id>/driver.js" %}

```javascript
const Homey = require('homey');

class Driver extends Homey.Driver {
  async onInit() {
    this._deviceTurnedOn = this.homey.flow.getDeviceTriggerCard("turned_on");
  }

  triggerMyFlow(device, tokens, state) {
    this._deviceTurnedOn
      .trigger(device, tokens, state)
      .then(this.log)
      .catch(this.error);
  }
}

module.exports = Driver;
```

{% endcode %}

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require('homey');

class Device extends Homey.Device {
  async onInit() {
    let device = this; // We're in a Device instance
    let tokens = {};
    let state = {};

    this.driver.ready().then(() => {
      this.driver.triggerMyFlow(device, tokens, state);
    });
  }
}

module.exports = Device;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/drivers/\<driver\_id>/driver.mts" %}

```mts
import Homey, { FlowCardTriggerDevice } from 'homey';
import type Device from './device.mjs';

export default class Driver extends Homey.Driver {
  private deviceTurnedOn?: FlowCardTriggerDevice;

  async onInit(): Promise<void> {
    this.deviceTurnedOn = this.homey.flow.getDeviceTriggerCard('turned_on');
  }

  triggerMyFlow(device: Device, tokens: object, state: object): void {
    this.deviceTurnedOn?.trigger(device, tokens, state);
  }
}

```

{% endcode %}

{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey from 'homey';
import type Driver from './driver.mjs';

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    (this.driver as Driver).triggerMyFlow(this, {}, {});
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/drivers/\<driver\_id>/driver.py" %}

```python
from homey import driver
from homey.flow_card_trigger_device import FlowCardTriggerDevice

from .device import Device


class Driver(driver.Driver):
    device_turned_on: FlowCardTriggerDevice | None

    async def on_init(self) -> None:
        self.device_turned_on = self.homey.flow.get_device_trigger_card("turned_on")

    async def trigger_my_flow(self, device: Device, tokens: dict, state: dict) -> None:
        if self.device_turned_on:
            await self.device_turned_on.trigger(device, tokens, **state)


homey_export = Driver

```

{% endcode %}

{% code title="/drivers/\<driver\_id>/device.py" %}

```python
from typing import cast

from homey import device

from .driver import Driver


class Device(device.Device):
    async def on_init(self) -> None:
        await cast(Driver, self.driver).trigger_my_flow(self, {}, {})


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

### Flow Card Device filters

If necessary you can add filters to choose for which devices the specific flow card will be available. The `$filter` option supports the following properties:

* `class` filtering based on the device class
* `capabilities` allows based on available capabilities (note that calls to `addCapability` and `removeCapability` don't update this filter)
* `flags` filters based on additional device properties, will be explained below

A simple filter might look like this:

{% code title="/drivers/\<driver\_id>/driver.flow\.compose.json" %}

```javascript
{
  "actions": [
    {
      "id": "disco_mode",
      "title": { "en": "Disco mode" },
      "$filter": "class=socket"
    }
  ]
}
```

{% endcode %}

Filters can match one of multiple values by separating values with a pipe (`|`), for example: `"class=socket|light"` will match devices with a device class of either `socket` or `light`.

`capabilities` and `flags` also support matching on multiple values by separating the values with a comma (`,`), for example `"capabilities=onoff,dim"` will match devices that have both `onoff` and `dim` capabilities.

Multiple properties can be filtered on by combining them in the `$filter` separated by an ampersand (`&`), for example this filter will match devices belonging to the `basic` driver with a `socket` or `light` device class and `onoff` and `dim` capabilities.

```javascript
"$filter": "class=socket|light&capabilities=onoff,dim"
```

The following filters can be applied to Z-Wave devices:

**Target only multi channel node devices:**

```javascript
"$filter": "flags=zwaveMultiChannel"
```

**Target only root node devices:**

```javascript
"$filter": "flags=zwaveRoot"
```

The following filters can be applied to Zigbee devices:

**Target only sub devices:**

```javascript
"$filter": "flags=zigbeeSubDevice"
```

## Highlighted Flow cards

Sometimes you will have a large list of Flow cards available in your app, finding the most important Flow cards might then become cumbersome for users. For cases like this, there is an option available to have some cards appear in a separate list above all other cards. This list is called "Highlighted Cards", highlighting the most important cards makes it easier for users to quickly find the most important ones.

Highlighting a Flow card is simply done by adding `"highlight": true` to your Flow card in the App Manifest.

Some built-in Flow cards are always highlighted, for example the “Motion alarm turned on” card.

{% hint style="warning" %}
Choose your highlighted Flow cards carefully, the point is to list a few cards that are used often. If you highlight too many cards this list can quickly become just as hard to navigate as the list of all Flow cards.
{% endhint %}

## Advanced Flow cards

To make a Flow card available only for use in Advanced Flow, add `"advanced": true` to your Flow card in the App Manifest.

{% hint style="info" %}
Then-cards with the `"tokens"` property automatically imply the `"advanced": true` property.
{% endhint %}

## Deprecating cards

Sometimes a Flow card that has been available in the past should be removed. To not break compatibility for users who were using it, add `"deprecated": true` to your Flow card in the App Manifest. It will still work, but won't show up anymore in the 'Add Card' list.


# Arguments

Flow arguments allow Flow cards to ask for user input.

Flow card arguments are passed as an input to your Flow card, users can pick the argument values when creating or editing a Flow.

Arguments must have a `type` that determines how they are shown. For example, a `dropdown` argument allows a user to pick one value from a predefined set of options, whereas a `text` argument shows an input field.

{% hint style="info" %}
Don't overuse arguments! In our experience, Flow cards with just one or two arguments are the most popular.
{% endhint %}

## Argument Types

The following are all available argument types that you can use in your Flow cards and the options that you can use with those types.

### Text

`"type": "text"`

Regular text input. Text, Number and Boolean tokens can be dropped in this field as well.

![](/files/-MZwd9D9M3CLyoQj9ifq)

**Attributes**

| Name        | Type                                                       | Description                | Example                                             |
| ----------- | ---------------------------------------------------------- | -------------------------- | --------------------------------------------------- |
| placeholder | [translation object](/the-basics/app/internationalization) | Text to show without input | `{ "en": "Hello, World!", "nl": "Hallo, Wereld!" }` |
| title       | [translation object](/the-basics/app/internationalization) | Text shown above argument  | `{ "en": "Sentence", nl": "Zin" }`                  |

**Example**

{% code title="/.homeycompose/flow/actions/greet.json" %}

```javascript
{
  "title": { "en": "Say a greeting" },
  "titleFormatted": { "en": "Say [[sentence]]" },
  "args": [
    {
      "type": "text",
      "name": "sentence",
      "title": { "en": "Sentence" },
      "placeholder": { "en": "Hello!" }
    }
  ]
}
```

{% endcode %}

### Autocomplete

`"type": "autocomplete"`

This is the same as a text input, but with an additional autocomplete popup. The returned value when the card is run, is one of the objects provided in the autocomplete array. In order to provide autocomplete results you need to register an autocomplete listener for the Flow card with [`FlowCard#registerArgumentAutocompleteListener()`](https://apps-sdk-v3.developer.homey.app/FlowCard.html#registerArgumentAutocompleteListener) or [`FlowCard#register_argument_autocomplete_listener()`](https://python-apps-sdk-v3.developer.homey.app/flow_card.html#homey.flow_card.FlowCard.register_argument_autocomplete_listener)

![](/files/-MZwhah13pHt4c-ajYn3)

**Attributes**

| Name        | Type                                                       | Description                | Example                                      |
| ----------- | ---------------------------------------------------------- | -------------------------- | -------------------------------------------- |
| placeholder | [translation object](/the-basics/app/internationalization) | Text to show without input | `{ "en": "YouTube", "nl": "Dumpert" }`       |
| title       | [translation object](/the-basics/app/internationalization) | Text shown above argument  | `{ "en": "Application", nl": "Applicatie" }` |

**Example**

{% code title="/.homeycompose/flow/actions/play\_artist.json" %}

```javascript
{
  "title": { "en": "Play an Artist" },
  "titleFormatted": { "en": "Play an Artist [[artist]]" },
  "args": [
    {
      "type": "autocomplete",
      "name": "artist",
      "title": { "en": "Artist" },
      "placeholder": { "en": "Ludwig van Beethoven" }
    }
  ]
}
```

{% endcode %}

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  async onInit() {
    const launchAppCard = this.homey.flow.getActionCard("play_artist");

    launchAppCard.registerArgumentAutocompleteListener(
      "artist",
      async (query, args) => {
        const results = [
          {
            name: "Wolfgang Amadeus Mozart",
            description: "Joannes Chrysostomus Wolfgangus Theophilus Mozart",
            icon: "https://path.to/icon.svg",
            // For images that are not svg use:
            // image: 'https://path.to/icon.png',

            // You can freely add additional properties
            // that you can access in registerRunListener
            id: "...",
          },
        ];

        // filter based on the query
        return results.filter((result) => {
           return result.name.toLowerCase().includes(query.toLowerCase());
        });
      }
    );
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/app.mts" %}

```mts
import Homey, { type FlowCard } from "homey";

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    const launchAppCard = this.homey.flow.getActionCard("play_artist");

    launchAppCard.registerArgumentAutocompleteListener(
      "artist",
      async (query: string, args): Promise<FlowCard.ArgumentAutocompleteResults> => {
        const results = [
          {
            name: "Wolfgang Amadeus Mozart",
            description: "Joannes Chrysostomus Wolfgangus Theophilus Mozart",
            icon: "https://path.to/icon.svg",
            // For images that are not svg use:
            // image: 'https://path.to/icon.png',

            // You can freely add additional properties
            // that you can access in registerRunListener
            id: "...",
          },
        ];

        // filter based on the query
        return results.filter(result => {
          return result.name.toLowerCase().includes(query.toLowerCase());
        });
      },
    );
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/app.py" %}

```python
from homey import app
from homey.flow_card import ArgumentAutocompleteResult


class App(app.App):
    async def on_init(self) -> None:
        launch_app_card = self.homey.flow.get_action_card("play_artist")

        async def autocomplete_listener(
            query, **card_args
        ) -> list[ArgumentAutocompleteResult]:
            results: list[ArgumentAutocompleteResult] = [
                {
                    "name": "Wolfgang Amadeus Mozart",
                    "description": "Joannes Chrysostomus Wolfgangus Theophilus Mozart",
                    "icon": "https://path.to/icon.svg",
                    # For images that are not svg use:
                    # image: 'https://path.to/icon.png',
                    "data": {
                        # You can freely add additional properties here
                        # that you can access in registerRunListener
                        "id": "...",
                    },
                },
            ]

            return [
                result for result in results if query.lower() in result["name"].lower()
            ]

        launch_app_card.register_argument_autocomplete_listener(
            "artist", autocomplete_listener
        )


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}

{% hint style="warning" %}
If you don't filter based on the query, it will cause the autocomplete results to appear unresponsive. The easiest way to filter the results is to compare `name` and `query` in lowercase as shown above.
{% endhint %}

### Number

`"type": "number"`

Regular text input. Tokens can be dropped in this field as well.

![](/files/-MZwdFkb4ywhyUPoCixK)

**Attributes**

| Name        | Type                                                       | Description                | Example                                                    |
| ----------- | ---------------------------------------------------------- | -------------------------- | ---------------------------------------------------------- |
| min         | `number`                                                   | Minimum input value        | 40                                                         |
| max         | `number`                                                   | Maximum input value        | 90                                                         |
| step        | `number`                                                   | Step size                  | 10                                                         |
| placeholder | [translation object](/the-basics/app/internationalization) | Text to show without input | `{ "en": "In degree celsius", "nl": "In graden celsius" }` |
| title       | [translation object](/the-basics/app/internationalization) | Text shown above argument  | `{ "en": "Temperature", nl": "Temperatuur" }`              |

**Example**

{% code title="/.homeycompose/flow/actions/wash\_clothes.json" %}

```javascript
{
  "title": { "en": "Wash clothes" },
  "titleFormatted": { "en": "Wash clothes at [[temperature]] degrees celsius" },
  "args": [
    {
      "type": "number",
      "name": "temperature",
      "title": { "en": "Temperature" },
      "placeholder": { "en": "In degree celsius" },
      "min": 40,
      "max": 90,
      "step": 10
    }
  ]
}
```

{% endcode %}

### Range

`"type": "range"`

A slider with a minimum and maximum value.

![](/files/-MZwdI4vKRSATscPdrn0)

**Attributes**

| Name            | Type                                                       | Description                                      | Example                                     |
| --------------- | ---------------------------------------------------------- | ------------------------------------------------ | ------------------------------------------- |
| min             | `number`                                                   | Minimum input value                              | 0                                           |
| max             | `number`                                                   | Maximum input value                              | 1                                           |
| step            | `number`                                                   | Step size                                        | 0.01                                        |
| label           | `string`                                                   | The units after the number                       | %                                           |
| labelMultiplier | `number`                                                   | Number is shown after multiplying by this factor | 100                                         |
| labelDecimals   | `number`                                                   | Number of decimals to round to                   | 2                                           |
| title           | [translation object](/the-basics/app/internationalization) | Text shown above argument                        | `{ "en": "Brightness", nl": "Helderheid" }` |

**Example**

{% code title="/.homeycompose/flow/actions/set\_brightness.json" %}

```javascript
{
  "title": { "en": "Set brightness" },
  "titleFormatted": { "en": "Set brightness to [[brightness]]" },
  "args": [
    {
      "type": "range",
      "name": "brightness",
      "title": { "en": "Brightness" },
      "min": 0,
      "max": 1,
      "step": 0.01,
      "label": "%",
      "labelMultiplier": 100,
      "labelDecimals": 2
    }
  ]
}
```

{% endcode %}

### Date

`"type": "date"`

Date input (presented in `dd-mm-yyyy`)

![](/files/-MZwdKAuB-EXzdkGRRxU)

**Attributes**

| Name        | Type                                                       | Description                | Example                                         |
| ----------- | ---------------------------------------------------------- | -------------------------- | ----------------------------------------------- |
| placeholder | [translation object](/the-basics/app/internationalization) | Text to show without input | `{ "en": "When to ..", "nl": "Wanneer te .." }` |
| title       | [translation object](/the-basics/app/internationalization) | Text shown above argument  | `{ "en": "Birthday", nl": "Verjaardag" }`       |

**Example**

{% code title="/.homeycompose/flow/actions/birthday\_surprise.json" %}

```javascript
{
  "title": { "en": "Birthday surprise" },
  "titleFormatted": { "en": "Surprise you on [[birthday]]" },
  "args": [
    {
      "type": "date",
      "name": "birthday",
      "title": { "en": "Birthday" },
      "placeholder": { "en": "18-05-1994" }
    }
  ]
}
```

{% endcode %}

### Time

`"type": "time"`

Time input (presented in `HH:mm`)

![](/files/-MZwdMBtixF3fjFXFW95)

**Attributes**

| Name        | Type                                                       | Description                | Example                                         |
| ----------- | ---------------------------------------------------------- | -------------------------- | ----------------------------------------------- |
| placeholder | [translation object](/the-basics/app/internationalization) | Text to show without input | `{ "en": "When to ..", "nl": "Wanneer te .." }` |
| title       | [translation object](/the-basics/app/internationalization) | Text shown above argument  | `{ "en": "Time", nl": "Tijd" }`                 |

**Example**

{% code title="/.homeycompose/flow/actions/activate\_alarm.json" %}

```javascript
{
  "title": { "en": "Activate the alarm" },
  "titleFormatted": { "en": "Activate the alarm at [[activationtime]]" },
  "args": [
    {
      "type": "time",
      "name": "activationtime",
      "title": { "en": "Time" },
      "placeholder": { "en": "13:37" }
    }
  ]
}
```

{% endcode %}

### Dropdown

`"type": "dropdown"`

A dropdown list with pre-defined values

![](/files/-MZwdP9PWpf1T11d_tJG)

**Attributes**

| Name   | Type                                                       | Description                 | Example                                                |
| ------ | ---------------------------------------------------------- | --------------------------- | ------------------------------------------------------ |
| values | `array`                                                    | An array of possible values | `[ { "id": "value1", "title": { "en": "Value 1" } } ]` |
| title  | [translation object](/the-basics/app/internationalization) | Text shown above argument   | `{ "en": "My title", nl": "Mijn titel" }`              |

**Example**

{% code title="/.homeycompose/flow/triggers/rain\_start.json" %}

```javascript
{
  "title": { "en": "It is going to rain in..." },
  "titleFormatted": { "en": "It is going to rain in [[when]]" },
  "args": [
    {
      "type": "dropdown",
      "name": "when",
      "title": { "en": "When it will rain" },
      "values": [
        { "id": "5", "title": { "en": "5 minutes" } },
        { "id": "10", "title": { "en": "10 minutes" } },
        { "id": "15", "title": { "en": "15 minutes" } }
      ]
    }
  ]
}
```

{% endcode %}

### Multiselect

`"type": "multiselect"`

A multiselect list with pre-defined values

![](/files/bIjBU37s0GH4df3343Id)

**Attributes**

| Name        | Type                                                       | Description                                                           | Example                                                |
| ----------- | ---------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------ |
| values      | `array`                                                    | An array of possible values                                           | `[ { "id": "value1", "title": { "en": "Value 1" } } ]` |
| title       | [translation object](/the-basics/app/internationalization) | Text shown above argument                                             | `{ "en": "My title", nl": "Mijn titel" }`              |
| conjunction | `string`                                                   | The conjunction of the argument in the preview for users (and or or). | `or`                                                   |

**Example**

{% code title="/.homeycompose/flow/conditions/today\_is\_a\_day.json" %}

```javascript
{
  "title": {
    "en": "Today is a"
  },
  "titleFormatted": {
    "en": "Today is a [[days]]"
  },
  "args": [
    {
      "title": {
        "en": "Day"
      },
      "name": "days",
      "type": "multiselect",
      "conjunction": "or",
      "values": [
        { "id": "mon", "title": { "en": "Monday" } },
        { "id": "tue", "title": { "en": "Tuesday" } },
        { "id": "wed", "title": { "en": "Wednesday" } },
        { "id": "thu", "title": { "en": "Thursday" } },
        { "id": "fri", "title": { "en": "Friday" } },
        { "id": "sat", "title": { "en": "Saturday" } },
        { "id": "sun", "title": { "en": "Sunday" } }
      ]
    }
  ]
}

```

{% endcode %}

### Checkbox

`"type": "checkbox"`

A dropdown list with a true and false option that supports boolean tokens.

![](/files/36biFZUJzyQ3OFFanrQt)

**Attributes**

<table data-header-hidden><thead><tr><th>Name</th><th width="225">Type</th><th>Description</th><th>Example</th></tr></thead><tbody><tr><td>Name</td><td>Type</td><td>Description</td><td>Example</td></tr><tr><td>title</td><td><a href="/pages/-MWdBAdwDRttE0KkWl48">translation object</a></td><td>Text shown above argument</td><td><code>{ "en": "My title", nl": "Mijn titel" }</code></td></tr></tbody></table>

**Example**

{% code title="/.homeycompose/flow/triggers/rain\_start.json" %}

```javascript
{
  "title": { "en": "Set enabled to..." },
  "titleFormatted": { "en": "Set enabled to [[enabled]]" },
  "args": [
    {
      "type": "checkbox",
      "name": "enabled",
      "title": { "en": "Enabled" }
    }
  ]
}
```

{% endcode %}

### Color

`"type": "color"`

A color picker that returns a HEX color, e.g. `#FF0000`.

![](/files/-MZwdSjlCt3MAxgBi1MD)

**Example**

{% code title="/.homeycompose/flow/actions/set\_tile\_color.json" %}

```javascript
{
  "title": { "en": "Set tile color" },
  "titleFormatted": { "en": "Set tile color to [[background]]" },
  "args": [
    {
      "type": "color",
      "name": "background"
    }
  ]
}
```

{% endcode %}

### Droptoken

A droptoken is a special Flow card Argument that only allows the user to enter a [Flow Token](/the-basics/flow/tokens) or Homey Logic variable. You can add a droptoken to your Flow card by specifying, for example, `"droptoken": ["number"]`. You can use the any of the following types: `string`, `number`, `boolean` or `image`. A Flow card can only have a single droptoken but you can specify multiple allowed types.

{% hint style="info" %}
Droptokens are possibly null. Make sure to verify the droptoken exists before using it.
{% endhint %}

![](/files/-Md1Hy7W9954uQlMYddK)

**Example**

{% code title="/.homeycompose/flow/conditions/equal.json" %}

```javascript
{
  "title": { "en": "Is equal" },
  "titleFormatted": { "en": "[[droptoken]] equals [[value]]" },
  "droptoken": ["number"],
  "args": [
    {
      "type": "number",
      "name": "value"
    }
  ]
}
```

{% endcode %}

### Device

`"type": "device"`

When you add a device argument to your flow card, and provide a `driver_id` filter, e.g. `"filter": "driver_id=yourdriverid"`, the Flow card will only be displayed for devices that belong to that specific driver.

If the device was already there because the device's class is a supported device class (e.g. `light`), your cards will be appended to the existing stack of cards. An example would be a Light driver that has a 'disco' mode, next to on/off, dim and color.

If the card has more than one device fields, the other fields will behave like an autocomplete-like argument, which show devices paired in your app.

See the [Flow guide Device cards section](/the-basics/flow#device-cards) for more information about device arguments and filters.

**Example**

{% code title="/.homeycompose/flow/actions/my\_action.json" overflow="wrap" %}

```javascript
{
  "title": {
    "en": "I will show up under all devices with a driver id of mydriver and with the custom capability id mycustomcapability."
  },
  "args": [
    {
      "type": "device",
      "name": "device",
      "title": { "en": "Device" },
      "filter": "driver_id=mydriver&capabilities=mycustomcapability"
    }
  ]
}
```

{% endcode %}

## Optional arguments

By default all Flow card arguments are required however if you want to allow an argument to be optional you can set the `required` property to `false`.

{% hint style="warning" %}
If an argument is not required and is not provided by the user it may be `undefined` in your Flow card run handler.
{% endhint %}

**Example**

{% code title="/.homeycompose/flow/actions/post\_data.json" %}

```javascript
{
  "title": { "en": "Post data to URL" },
  "titleFormatted": { "en": "Post [[data]] to [[url]]" },
  "args": [
    {
      "type": "text",
      "name": "url",
      "title": { "en": "URL" },
      "placeholder": { "en": "https://example.com" }
    }
    {
      "type": "text",
      "name": "data",
      "required": false,
      "title": { "en": "Body" },
      "placeholder": { "en": "message" }
    }
  ]
}
```

{% endcode %}

## Action Card duration

If you are creating an Action Flow card, a card for the *...then* column, you can set the `duration` property. This property allows users to choose a duration for the action. If a user provides this argument to the Flow card, it will be passed to the card handler as an argument named `duration` in milliseconds.

{% hint style="warning" %}
`"duration": true` has precedence over an argument with `"name": "duration"` as they are both provided as arguments to the same handler. You should not create a Flow card with the `duration` property and an argument named `"duration".`
{% endhint %}

**Example**

{% code title="/.homeycompose/flow/actions/run\_animation.json" %}

```javascript
{
  "title": { "en": "Run animation" },
  "titleFormatted": { "en": "Run animation [[animation]]" },
  "duration": true,
  "args": [
    {
      "type": "dropdown",
      "name": "animation",
      "title": { "en": "Animation" },
      "values": [
        { "id": "rainbow", "title": { "en": "Rainbow" } },
        { "id": "kitt", "title": { "en": "KITT" } },
        { "id": "pulse", "title": { "en": "Pulse" } }
      ]
    }
  ]
}
```

{% endcode %}

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  async onInit() {
    const runAnimationAction = this.homey.flow.getActionCard("run_animation");

    runAnimationAction.registerRunListener(async (args, state) => {
      if (args.duration != null) {
        // do something with the duration
        // (e.g. run an animation for duration milliseconds)
      }
    });
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/app.mts" %}

```mts
import Homey from "homey";

type RunAnimationArgs = {
  duration: number;
};

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    const runAnimationAction = this.homey.flow.getActionCard("run_animation");

    runAnimationAction.registerRunListener(async (args: RunAnimationArgs, state) => {
      if (args.duration != null) {
        // do something with the duration
        // (e.g. run an animation for duration milliseconds)
      }
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/app.py" %}

```python
from homey import app


class App(app.App):
    async def on_init(self) -> None:
        run_animation_action = self.homey.flow.get_action_card("run_animation")

        async def run_listener(card_arguments, **trigger_kwargs) -> None:
            if card_arguments.get("duration") is not None:
                # do something with the duration
                # (e.g. run an animation for duration milliseconds)
                ...

        run_animation_action.register_run_listener(run_listener)


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}

## Subscribing to argument changes

It might be useful to know when a trigger has changed. For example a Twitter app may have a Flow card that triggers a Flow when a specific hashtag is tweeted. In order to implement this behaviour the app needs to know what hashtags to search for. By subscribing to argument changes the app knows when the argument values have changed and update the list of hashtags it is looking for.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  async onInit() {
    const myTrigger = this.homey.flow.getTriggerCard("my_trigger");

    myTrigger.on("update", () => {
      this.log("update");

      myTrigger.getArgumentValues().then((args) => {
        // args is [{ "my_arg": "user_value" }]
      });
    });
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/app.mts" %}

```mts
import Homey from "homey";

type FlowCardArguments = {
  my_arg: string;
};

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    const myTrigger = this.homey.flow.getTriggerCard("my_trigger");

    myTrigger.on("update", () => {
      this.log("update");

      myTrigger.getArgumentValues().then((args: FlowCardArguments[]) => {
        // args is [{ "my_arg": "user_value" }]
      });
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/app.py" %}

```python
import asyncio

from homey import app


class App(app.App):
    async def on_init(self) -> None:
        my_trigger = self.homey.flow.get_trigger_card("my_trigger")

        async def handle_updated_argument_values():
            args = await my_trigger.get_argument_values()
            # args is ({"my_arg": "user_value"},)

        def on_update():
            self.log("update")
            asyncio.create_task(handle_updated_argument_values())

        my_trigger.on_update(on_update)


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}

## Flow State

When a Flow is triggered and the Flow Trigger card has arguments, your app needs to validate that the current state matches with the arguments the user chose for the Flow. This listener is executed for each Flow that uses this Flow Trigger card.

For example, a `rain_start` Flow Trigger card can have a `location` argument so that the Flow is only triggered when it is raining in the place the user has selected.

{% code title="/.homeycompose/flow/triggers/rain\_start.json" %}

```javascript
{
  "title": { "en": "It starts raining" },
  "tokens": [
    {
      "name": "mm_per_hour",
      "type": "number",
      "title": { "en": "mm/h" },
      "placeholder": { "en": "5" }
    }
  ],
  "args": [
    {
      "name": "location",
      "type": "text"
    }
  ]
}
```

{% endcode %}

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/app.js" %}

```javascript
const Homey = require('homey');
const RainApi = require('rain-api');

class App extends Homey.App {
  async onInit() {
    const rainStartTrigger = this.homey.flow.getTriggerCard("rain_start");

    rainStartTrigger.registerRunListener(async (args, state) => {
      // args is the user input,
      // for example { 'location': 'New York' }
      // state is the parameter passed in trigger()
      // for example { 'location': 'Amsterdam' }

      // If true, this flow should run
      return args.location === state.location;
    });

    RainApi.on('raining', (city, amount) => {
      const tokens = { mm_per_hour: amount }; // for example 3
      const state = { location: city }; // for example "Amsterdam"

      // trigger the card
      rainStartTrigger.trigger(tokens, state)
        .then(this.log)
        .catch(this.error);
    });
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/app.mts" %}

```mts
import Homey from "homey";
import RainApi from "rain-api";

type RainStartArguments = {
  location: string;
};

type RainStartState = {
  location: string;
};

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    const rainStartTrigger = this.homey.flow.getTriggerCard("rain_start");

    rainStartTrigger.registerRunListener(async (args: RainStartArguments, state: RainStartState) => {
      // args is the user input,
      // for example { 'location': 'New York' }
      // state is the parameter passed in trigger()
      // for example { 'location': 'Amsterdam' }

      // If true, this flow should run
      return args.location === state.location;
    });

    RainApi.on("raining", (city: string, amount: number) => {
      const tokens = { mm_per_hour: amount }; // for example 3
      const state = { location: city }; // for example "Amsterdam"

      // trigger the card
      rainStartTrigger.trigger(tokens, state).then(this.log).catch(this.error);
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/app.py" %}

```python
import asyncio

from homey import app

from rain_api import RainApi


class App(app.App):
    async def on_init(self) -> None:
        rain_start_trigger = self.homey.flow.get_trigger_card("rain_start")

        async def run_listener(card_arguments, **trigger_kwargs) -> bool:
            # card_arguments is the user input
            # for example {"location": "New York"}
            # trigger_kwargs are the parameters passed in trigger()
            # for example {"location": "Amsterdam"}
            return card_arguments.get("location") == trigger_kwargs.get("location")

        rain_start_trigger.register_run_listener(run_listener)

        async def on_raining(city: str, amount: float) -> None:
            tokens = {"mm_per_hour": amount}  # for example 3
            try:
                self.log(
                    await rain_start_trigger.trigger(
                        tokens,
                        location=city,  # for example "Amsterdam"
                    )
                )
            except Exception as e:
                self.error(e)

        RainApi.on("raining", on_raining)


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}


# Tokens

Flow Tokens allow users to pass information between different Flow cards when creating their Flows.

Tokens are typed variables that can be used throughout a Flow. A token is either:

* **Local** — Attached to a Flow's trigger card, for example the name of the user that came home.
* **Global** — Available anywhere, for example the current time.

Both tokens can be used in a Flow card's argument, such as a textfield.

![](/files/-MZwkQADAZuYlPzIZYiW)

{% hint style="info" %}
In the User Interface, tokens are called *Tags.*
{% endhint %}

## Local Tokens

When triggering a Flow, some information might be useful to use in that Flow. For example, when it starts raining, the *mm/hour* could be a token.

Tokens have a pre-defined `type`, which can be either `string`, `number`, `boolean` or `image`.

Users can use Flow Tokens in compatible Flow Arguments, such as a *text* field for `string` tokens. This way, the user can make advanced flows with variables. To add tokens to a Flow card, just add a property `tokens` to your Flow card manifest.

{% hint style="info" %}
A Flow card can also have a`droptoken` field, droptokens are Flow Arguments that require the input to be a token. You can read more about droptokens in the [Flow Arguments documentation](/the-basics/flow/arguments#droptoken).
{% endhint %}

{% code title="/.homeycompose/flow/triggers/rain\_start.json" %}

```javascript
{
  "title": { "en": "It starts raining" },
  "tokens": [
    {
      "name": "mm_per_hour",
      "type": "number",
      "title": { "en": "mm/h" },
      "example": 5
    },
    {
      "name": "city",
      "type": "string",
      "title": { "en": "City" },
      "example": { "en": "Amsterdam" }
    }
  ]
}
```

{% endcode %}

And when firing your trigger, add them as argument:

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/app.js" %}

```javascript
const Homey = require('homey');
const RainApi = require('rain-api');

class App extends Homey.App {
  async onInit() {
    const rainStartTrigger = this.homey.flow.getTriggerCard("rain_start");

    RainApi.on('raining', (city, amount) => {
      const tokens = {
        mm_per_hour: amount,
        city: city
      };

      rainStartTrigger.trigger(tokens)
        .then(this.log)
        .catch(this.error);
    });
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/app.mts" %}

```mts
import Homey from "homey";
import RainApi from "rain-api";

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    const rainStartTrigger = this.homey.flow.getTriggerCard("rain_start");

    RainApi.on("raining", (city: string, amount: number) => {
      const tokens = {
        mm_per_hour: amount,
        city: city
      };

      rainStartTrigger.trigger(tokens)
        .then(this.log)
        .catch(this.error);
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/app.py" %}

```python
from homey import app

from rain_api import RainApi


class App(app.App):
    async def on_init(self) -> None:
        rain_start_trigger = self.homey.flow.get_trigger_card("rain_start")

        async def on_raining(city: str, amount: float) -> None:
            tokens = {
                "mm_per_hour": amount,
                "city": city
            }
            try:
                self.log(await rain_start_trigger.trigger(tokens))
            except Exception as e:
                self.error(e)

        RainApi.on("raining", on_raining)


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}

### Tokens for Advanced Flow

A Then-card can optionally return tokens in an [Advanced Flow](https://homey.app/advanced-flow/). Similar to a When-card, specify the `tokens` property as an `array`, and return an `object` in the run-listener of your Flow card.

```json
{
  "title": {
    "en": "Make it stop raining"
  },
  "hint": {
    "en": "Hires a shaman to do a sunny dance. Might cost some money."
  },
  "tokens": [
    {
      "name": "shamanName",
      "type": "string",
      "title": {
        "en": "Name of the Shaman"
      }
    },
    {
      "name": "shamanCost",
      "type": "number",
      "title": {
        "en": "Cost of the Shaman (€)"
      }
    }
  ]
}
```

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/app.js" %}

```javascript
    const stopRainingAction = this.homey.flow.getActionCard('stop_raining');
    stopRainingAction.registerRunListener(async (args, state) => {
      await RainApi.makeItStopRaining();
      
      // Return the Tokens for Advanced Flow
      return {
        shamanName: 'Alumbrada',
        shamanCost: 10,
      };
    });
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/app.mts" %}

```mts
    type StopRainingTokens = {
      shamanName: string;
      shamanCost: number;
    };
    const stopRainingAction = this.homey.flow.getActionCard("stop_raining");
    stopRainingAction.registerRunListener(async (args, state): Promise<StopRainingTokens> => {
      await RainApi.makeItStopRaining();

      // Return the Tokens for Advanced Flow
      return {
        shamanName: "Alumbrada",
        shamanCost: 10,
      };
    });
```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/app.py" %}

```python
from homey import app

from rain_api import RainApi


class App(app.App):
    async def on_init(self) -> None:
        stop_raining_action = self.homey.flow.get_action_card("stop_raining")

        async def run_listener(card_arguments, **trigger_kwargs) -> dict:
            await RainApi.make_it_stop_raining()

            # Return the Tokens for Advanced Flow
            return {
                "shamanName": "Alumbrada",
                "shamanCost": 10,
            }

        stop_raining_action.register_run_listener(run_listener)


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}

{% hint style="info" %}
When specifying a `tokens` array in your Then-card's JSON, that card will not be visible when creating & editing a standard Flow.
{% endhint %}

## Global Tokens

A token can also be registered globally, so they can be used in any Flow. This means the token can be used without requiring the app to trigger the Flow. For example a weather app could expose the current temperature as a global token.

By default a device's capability are registered as global tokens.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  async onInit() {
    const myToken = await this.homey.flow.createToken("my_token", {
      type: "number",
      title: "My Token",
    });

    await myToken.setValue(23.5);
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/app.mts" %}

```mts
import Homey from "homey";

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    const myToken = await this.homey.flow.createToken("my_token", {
      type: "number",
      title: "My Token",
    });

    await myToken.setValue(23.5);
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/app.py" %}

```python
from homey import app


class App(app.App):
    async def on_init(self) -> None:
        my_token = await self.homey.flow.create_token("my_token", "number", "My Token")

        await my_token.set_value(23.5)


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}

## Image Tokens

Certain applications might want to share images between Flows, e.g. a webcam that made a snapshot, or a Chromecast that wants to cast an image. A token with type `image` can be used here.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  async onInit() {
    const myImage = await this.homey.images.createImage();
    myImage.setPath(path.join(__dirname, "assets", "images", "kitten.jpg"));

    // create a token & register it
    const myImageToken = await this.homey.flow.createToken("my_token", {
      type: "image",
      title: "My Image Token",
    });

    await myImageToken.setValue(myImage);

    // listen for a Flow action
    const myActionCard = this.homey.flow.getActionCard("image_action");

    myActionCard.registerRunListener(async (args, state) => {
      // get the contents of the image
      const imageStream = await args.droptoken.getStream();
      this.log(`saving ${imageStream.meta.contentType} to: ${imageStream.meta.filename}`);

      // save the image
      const targetFile = fs.createWriteStream(
        path.join("/userdata", imageStream.meta.filename)
      );
      imageStream.pipe(targetFile);
      return true;
    });

    const myTriggerCard = this.homey.flow.getTriggerCard("image_trigger");

    // pass the image to the trigger call
    await myTriggerCard.trigger({ my_image: myImage });
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/app.mts" %}

```mts
import Homey, { type Image } from "homey";
import path from "node:path";
import * as fs from "node:fs";

type ImageActionArgs = {
  droptoken: Image;
};

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    const myImage = await this.homey.images.createImage();
    myImage.setPath(path.join(__dirname, "assets", "images", "kitten.jpg"));

    // create a token & register it
    const myImageToken = await this.homey.flow.createToken("my_token", {
      type: "image",
      title: "My Image Token",
      value: myImage,
    });

    // listen for a Flow action
    const myActionCard = this.homey.flow.getActionCard("image_action");

    myActionCard.registerRunListener(async (args: ImageActionArgs, state) => {
      // get the contents of the image
      const imageStream = await args.droptoken.getStream();
      this.log(`saving ${imageStream.meta.contentType} to: ${imageStream.meta.filename}`);

      // save the image
      const targetFile = fs.createWriteStream(path.join("/userdata", imageStream.meta.filename));
      imageStream.pipe(targetFile);
      return true;
    });

    const myTriggerCard = this.homey.flow.getTriggerCard("image_trigger");

    // pass the image to the trigger call
    await myTriggerCard.trigger({ my_image: myImage });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/app.mts" %}

```python
import os
from typing import TypedDict

from homey import app
from homey.image import Image


class ImageActionArgs(TypedDict):
    droptoken: Image


class App(app.App):
    async def on_init(self) -> None:
        my_image = await self.homey.images.create_image()
        my_image.set_path(
            os.path.join(os.path.dirname(__file__), "assets", "images", "kitten.jpg")
        )

        my_image_token = await self.homey.flow.create_token(
            "my_token", "image", "My Image Token", my_image
        )

        my_action_card = self.homey.flow.get_action_card("image_action")

        async def run_listener(card_arguments: ImageActionArgs, **trigger_kwargs):
            image_stream = await card_arguments.get("droptoken").get_stream()
            self.log(
                f"saving {image_stream['meta']['contentType']} to {image_stream['meta']['filename']}"
            )

            # save the image
            with open(
                os.path.join("/userdata", image_stream["meta"]["filename"]), "wb"
            ) as target_file:
                target_file.write(image_stream["data"].buffer.read())

        my_action_card.register_run_listener(run_listener)

        my_trigger_card = self.homey.flow.get_trigger_card("image_trigger")
        # pass the image to the trigger call
        await my_trigger_card.trigger({"my_image": my_image})


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}


# Widgets

Widgets enable the creation of custom webviews for user dashboards.

Homey Apps can include custom widgets, which are standard web pages (HTML/CSS/JS) displayed to your app's users. These pages have access to a global Homey API, allowing them to communicate with Homey and your app. For instance, you could create a garbage collection calendar widget to show users the next pickup date.

{% hint style="info" %}
Widgets do not work on Homey Cloud and require a [compatibility](/the-basics/app/manifest#properties) of `>=12.3.0`.
{% endhint %}

## Creating a Widget

To create a widget, run the `homey app widget create` command in the Homey CLI. This command will prompt you for your widget's name and ID after which it will create a widget under `/widgets/<widgetId>/`. Increase the [compatibility](/the-basics/app/manifest#properties) of your app to `>=12.3.0` since widget support was added in that release. The widget will contain the following files:

* `widget.compose.json`
* `public/index.html`
* `api.js`
* `preview-dark.png`
* `preview-light.png`

### widget.compose.json

The `widget.compose.json` file contains the definition of your widget. Here, you can define the name, settings, height, transparent and API properties of the widget:

{% code title="/widgets/<widgetId>/widget.compose.json" %}

```json
{
  "name": {
    "en": "My Widget"
  },
  "settings": [
    {
      "id": "my-id",
      "type": "text",
      "title": {
        "en": "My Title"
      }
    }
  ],
  "height": 100,
  "api": {
    "getSomething": {
      "method": "GET",
      "path": "/"
    },
    "addSomething": {
      "method": "POST",
      "path": "/"
    },
    "updateSomething": {
      "method": "PUT",
      "path": "/:id"
    },
    "deleteSomething": {
      "method": "DELETE",
      "path": "/:id"
    }
  }
}
```

{% endcode %}

Widgets can have settings that users can change while selecting or editing a widget. This allows you to add some initial variables to your widgets. These work almost the same as [device settings](/the-basics/devices/settings). For a full overview see [Settings](/the-basics/widgets/settings).

A height can be provided to set the initial height of the widget on load. It's also possible to provide the height during runtime. It's not advised to use both as that would cause shifts in the height during load. After first load the height will be cached so next loads will have no layout shifts. If the height is a number it is treated as an absolute value. If the height is in percentage it's used as an aspect ratio. So a height of 100% means a square widget.

In addition to setting the widget's height, you can also configure the widget's background transparency by setting the transparent property. By default, widgets have an opaque background, but setting `transparent: true` allows you to make the widget background fully transparent.

The `api` field contains the specification of your widget's API. This can be used to communicate with your app. These endpoints are scoped to the widget and not global.

### index.html

The `index.html` file will be the entry point to your widget. This file will be loaded as soon as a user's dashboard requests your widget. Anything under the `public` folder will be hosted on the user's Homey so place assets that are referenced from your `index.html` there.

### api.js

The `api.js` file contains the implementation of your API as defined in the `widget.compose.json` file. For each endpoint, you can add an implementation that performs any desired actions. As with the default [app api](/advanced/web-api) you have access to the `homey` instance of your app.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/widgets/<widgetId>/api.js" %}

```javascript
'use strict';

module.exports = {
  async getSomething({ homey, query }) {
    // you can access query parameters like "/?foo=bar" through `query.foo`

    // you can access the App instance through homey.app
    // const result = await homey.app.getSomething();
    // return result;

    // perform other logic like mapping result data

    return 'Hello from App';
  },

  async addSomething({ homey, body }) {
    // access the post body and perform some action on it.
    return homey.app.addSomething(body);
  },

  async updateSomething({ homey, params, body }) {
    return homey.app.setSomething(body);
  },

  async deleteSomething({ homey, params }) {
    return homey.app.deleteSomething(params.id);
  },
};
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/widgets/<widgetId>/api.mts" %}

```mts
import type App from "../../app.mjs";

type RequestWithBody = {
  homey: App["homey"];
  query: Record<string, string>;
  params: Record<string, string>;
  body: Record<string, unknown>;
};

type RequestWithoutBody = {
  homey: App["homey"];
  query: Record<string, string>;
  params: Record<string, string>;
  body: Record<never, never>; // Homey.API sends an empty body for GET and DELETE requests
};

export default {
  async getSomething({ homey, query }: RequestWithoutBody): Promise<any> {
    // you can access query parameters like "/?foo=bar" through `query.foo`

    // you can access the App instance through homey.app
    // const result = await homey.app.getSomething();
    // return result;

    // perform other logic like mapping result data

    return "Hello from App";
  },

  async addSomething({ homey, body }: RequestWithBody): Promise<any> {
    // access the post body and perform some action on it.
    return (homey.app as App).addSomething(body);
  },

  async updateSomething({ homey, params, body }: RequestWithBody): Promise<any> {
    return (homey.app as App).updateSomething(body);
  },

  async deleteSomething({ homey, params }: RequestWithoutBody): Promise<any> {
    return (homey.app as App).deleteSomething(params.id);
  },
};

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/widgets/<widgetId>/api.py" %}

```python
from typing import Any, Never, cast

from homey.homey import Homey

from ...app import App


async def get_something(
    *,
    homey: Homey,
    query: dict[str, str],
    params: dict[str, str],
    body: dict[Never, Never],  # Homey.API sends an empty body for GET requests
) -> Any:
    return "Hello from App"


async def add_something(
    *, homey: Homey, query: dict[str, str], params: dict[str, str], body: dict[str, Any]
) -> Any:
    return cast(App, homey.app).add_something(body)


async def update_something(
    *, homey: Homey, query: dict[str, str], params: dict[str, str], body: dict[str, Any]
) -> Any:
    return cast(App, homey.app).update_something(body)


async def delete_something(
    *,
    homey: Homey,
    query: dict[str, str],
    params: dict[str, str],
    body: dict[Never, Never],  # Homey.API sends an empty body for DELETE requests
) -> Any:
    return cast(App, homey.app).delete_something(params["id"])


# Export all these methods as endpoints
__all__ = ["get_something", "add_something", "update_something", "delete_something"]

```

{% endcode %}
{% endtab %}
{% endtabs %}

### preview-\<mode>.png

The images `preview-dark.png` and `preview-light.png` are used for previewing your widget in dark and light modes, respectively. Ensure they accurately represent the appearance of your widget. You can use the following Figma template as a starter [template](https://www.figma.com/community/file/1392859749687789493/widget-previews-template).

## Widget Instance

Each time a user adds a widget to a dashboard a unique id is generated, this id can be accessed via the `Homey.getWidgetInstanceId()` method. This can for example be used to save some data on the app side.\
\
On initial load of your widget a loading state will be displayed. When you call `Homey.ready()` this state will be removed so you can use this to do some initialization for your widget. This method can optionally be called with an object that specifies the height of the widget (`Homey.ready({ height: 200 })`). This height will override the height defined in the `widget.compose.json`.

{% code title="index.html" %}

```html
<html>

<head>
...
</head>

<body>
  <div id="message"></div>
  <button id="my-button">Button</button>

  <script type="text/javascript">
    function onHomeyReady(Homey) {
      Homey.ready({ height: 200 });

      console.log('instanceId: ', Homey.getWidgetInstanceId());
      console.log('settings', Homey.getSettings());

      document.getElementById('my-button').addEventListener('click', () => {
        Homey.api('GET', '/', {})
          .then((result) => {
            document.getElementById('message').innerText = String(result);
          })
          .catch(console.error);
      });
    }
  </script>
</body>

</html>
```

{% endcode %}

## Translations

Translations work the same as with custom pairing views or custom app settings views. See [Translating a string in Custom views](/the-basics/app/internationalization#translating-a-string-in-custom-views).

## Styling

To ensure a unified and consistent look across your widgets, we have created [a CSS styling solution](/the-basics/widgets/styling).

## Debugging

To enhance the developer experience, you can attach a webview debugger during widget development, which allows for inspecting and debugging your widget through tools like Chrome DevTools. For more details, see [Debugging](/the-basics/widgets/debugging).

When running `homey app run`, a **refresh button** will appear, allowing you to reload the `index.html` file without restarting the entire app.

* **Docker Requirement**: Docker is required to support this functionality, and `homey app run` will automatically enforce Docker usage.
* **Compatibility**:
  * **Supported Models**: This feature works only on Homey 2023 and later models.
  * **Unsupported Scenarios**:
    * It will **not function** on Homey models earlier than 2023.
    * It is **not available** when running the app remotely using `homey app run --remote`.

This applies only to files in the `public` folder, streamlining the testing process by enabling quick reloads and reducing full app restarts during development. For other file changes, a full restart is required.

## Deprecating Widgets

Widgets can be deprecated by setting **`deprecated: true`** in their configuration. This prevents users from selecting the widget when adding new ones, though existing instances will remain functional.

## View API

### Homey.ready

```javascript
Homey.ready(args?: { height: number | string }): void
```

Call this method when your widget is ready to be shown.

### Homey.api

```javascript
Homey.api(method: string, path: string, body?: object): Promise<unknown>
```

Access your api as defined under `widget.compose.json` -> `api`.

### Homey.on

```javascript
Homey.on(event: string, callback: (...args[]: any) => void): void
```

Listen to events emitted by your app.

### Homey.\_\_

```javascript
Homey.__(input: string, tokens?: object): string
```

Translate a string programmatically. The first argument `input` is the name in your `/locales/__language__.json`. Use dots to get a sub-property, e.g. `settings.title`. The optional second argument `tokens` is an object with replacers. Read more about translations in the [internationalization guide](/the-basics/app/internationalization).

### Homey.getWidgetInstanceId

```javascript
Homey.getWidgetInstanceId(): string
```

Get the unique id for the instance of the widget.

### Homey.getSettings

```javascript
Homey.getSettings(): { [key: string]: unknown }
```

Get the settings for your widget as filled in by the user.

### Homey.setHeight

```javascript
Homey.setHeight(height: number | string | null): Promise<void>
```

Change the widget height during runtime.

### Homey.popup

```javascript
Homey.popup(url: string): Promise<void>
```

Open an in app browser view.

### Homey.hapticFeedback

```javascript
Homey.hapticFeedback(): void
```

Provide a haptic feedback on presses. This function can only be called in a short window after a touch event.

### Homey.getDeviceIds

```javascript
Homey.getDeviceIds(): string[]
```

Retrieves the IDs of the devices selected by the user in the widget's **Devices** setting. See [Settings](/the-basics/widgets/settings#devices)


# Settings

Widget settings allow users to customize the behaviour of their dashboard widgets from Homey.

Widgets can have settings that users can change while selecting or editing a widget. This allows you to add some initial variables to your widgets.

<pre class="language-json" data-title="widget.compose.json"><code class="lang-json"><strong>{
</strong>  "name": {
    "en": "My Widget"
  },
  "settings": [
    {
      "id": "text",
      "type": "text",
      "title": {
        "en": "Text"
      },
      "hint": {
        "en": "Your number"
      }
    },
    {
      "id": "number",
      "type": "number",
      "title": {
        "en": "Number"
      },
      "hint": {
        "en": "Your number"
      }
    }
  ]
}
</code></pre>

## Defining Settings

Every setting has a `type` property that determines what values it can have and how it is presented to the user. The `value` property of each setting is the initial value of the setting. The following setting types are supported:

### Text

`string | null`

This is a single line text input whose value is a string. You can optionally validate the value using a regex pattern by adding a `pattern` property.

```json
{
  "id": "text",
  "type": "text",
  "title": { "en": "Text" },
  "value": "My initial value",
  "hint": { "en": "My text hint." },
  "pattern": "[a-zA-Z]"
}
```

### Textarea

`string | null`

This setting type allows users to input multi-line text.

```json
{
  "id": "description",
  "type": "textarea",
  "title": { "en": "Textarea" },
  "value": "Enter your description here.",
  "hint": { "en": "Provide a detailed description." },
  "pattern": "[a-zA-Z]"
}
```

### Number

`number | null`

This setting type allows users to input numerical values. The `min` and `max` properties are optional and can be used to define the acceptable range of values.

```json
{
  "id": "age",
  "type": "number",
  "title": { "en": "Age" },
  "value": 25,
  "hint": { "en": "Your age." },
  "min": 0,
  "max": 120
}
```

### Dropdown

`string | null`

This setting type allows users to select a value from a predefined list.

```json
{
  "id": "dropdown",
  "type": "dropdown",
  "title": {
    "en": "Dropdown",
    "nl": "Dropdown"
  },
  "value": "heating",
  "values": [
    {
      "id": "heating",
      "title": {
        "en": "Heating"
      }
    },
    {
      "id": "cooling",
      "title": {
        "en": "Cooling"
      }
    }
  ]
}
```

### Checkbox

`boolean | null`

This setting type allows users to enable or disable a feature.

```json
{
  "id": "checkbox",
  "type": "checkbox",
  "value": true,
  "title": {
    "en": "Checkbox",
    "nl": "Checkbox"
  }
}
```

### Autocomplete

`object | null`

This setting type provides an input that suggests options as the user types.

```json
{
  "id": "composer",
  "type": "autocomplete",
  "title": {
    "en": "Composer"
  }
}
```

{% tabs %}
{% tab title="JavaScript" %}

```javascript
'use strict';

const Homey = require('homey');

class MyApp extends Homey.App {

  async onInit() {
    const widget = this.homey.dashboards.getWidget('my-widget')

    widget.registerSettingAutocompleteListener('composer', async (query, settings) => {
      return [
        {
          name: "Mozart",
          // Optionally provide the following properties.
          description: "...",
          image: "https://some.url/",

          // You can freely add additional properties
          // that you can access in Homey.getSettings()['mySettingId'].
          id: "mozart",
        },
        {
          name: "Amadeus",

          // You can freely add additional properties
          // that you can access in Homey.getSettings()['mySettingId'].
          id: "amadeus",
        },
      ].filter((item) => item.name.toLowerCase().includes(query.toLowerCase()));
    });
  }
}

module.exports = MyApp;
```

{% endtab %}

{% tab title="TypeScript" %}

```mts
import Homey, { Widget } from "homey";

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    const widget = this.homey.dashboards.getWidget("my-widget");

    widget.registerSettingAutocompleteListener(
      "composer",
      async (query: string, settings): Promise<Widget.SettingAutocompleteResults> => {
        return [
          {
            name: "Mozart",
            // Optionally provide the following properties.
            description: "...",
            image: "https://some.url/",

            // You can freely add additional properties
            // that you can access in Homey.getSettings()['mySettingId'].
            id: "mozart",
          },
          {
            name: "Amadeus",

            // You can freely add additional properties
            // that you can access in Homey.getSettings()['mySettingId'].
            id: "amadeus",
          },
        ].filter(item => item.name.toLowerCase().includes(query.toLowerCase()));
      },
    );
  }
}

```

{% endtab %}

{% tab title="Python" %}

```python
from homey import app
from homey.widget import SettingAutocompleteResult


class App(app.App):
    async def on_init(self) -> None:
        widget = self.homey.dashboards.get_widget("my-widget")

        async def autocomplete_listener(
            query, settings
        ) -> list[SettingAutocompleteResult]:
            results: list[SettingAutocompleteResult] = [
                {
                    "name": "Mozart",
                    # Optionally provide the following properties.
                    "description": "...",
                    "image": "https://some.url/",
                    "data": {
                        # You can freely add additional properties
                        # that you can access in Homey.getSettings()['mySettingId'].data
                        "id": "mozart",
                    },
                },
                {
                    "name": "Amadeus",
                    "data": {
                        # You can freely add additional properties
                        # that you can access in Homey.getSettings()['mySettingId'].data
                        "id": "amadeus",
                    },
                },
            ]

            return [
                result for result in results if query.lower() in result["name"].lower()
            ]

        widget.register_setting_autocomplete_listener("artist", autocomplete_listener)


homey_export = App

```

{% endtab %}
{% endtabs %}

## Accessing Settings

{% code title="index.html" %}

```html
<html>

<head>
...
</head>

<body>
  <script type="text/javascript">
    function onHomeyReady(Homey) {
      Homey.ready();
    
      for (const [settingId, settingValue] of Object.entries(Homey.getSettings())) {
        // Do something with the settings...
      }
    }
  </script>
</body>

</html>
```

{% endcode %}

## Devices

Many widgets are designed to display content based on specific Homey devices. To simplify this, a dedicated `devices` setting is provided, allowing users to select one or more devices. Once selected, your widget can access their IDs using `Homey.getDeviceIds()`.

**Configuration Options**

The `devices` setting includes the following required properties:

* **`singular` (boolean, required)** – Determines whether users can select multiple devices:
  * `false` – Allows selecting multiple devices.
  * `true` – Restricts selection to a single device.
* **`type` (string, required)** – Defines the scope of selectable devices:
  * `"app"` – Limits selection to devices belonging to your app.
  * `"global"` – Allows selection from all Homey devices, only makes sense if the app has the necessary `homey:manager:api` permission.

Additionally, an **optional** `filter` property can be used to limit the selection to specific devices:

* **`filter` (object, optional)** – Defines selection criteria:
  * **`class` (string, optional)** – Filters devices by their device class (e.g., `"socket"`, `"light"`, `"sensor"`).
    * Supports multiple values using a **pipe (`|`)** as an **OR** separator (e.g., `"socket|light"` allows selecting both sockets and lights).
  * **`capabilities` (string, optional)** – Filters devices based on required capabilities (e.g., `"onoff"`).
    * Supports:
      * **OR (`|`)**: `"onoff|dim"` → Devices must have **at least one** of these capabilities.
      * **AND (`,`)**: `"onoff,dim"` → Devices must have **all** these capabilities.
      * **Combination (`|` and `,`)**: `"onoff,dim|light_mode"` → Devices must have **both** `onoff` and `dim` capabilities **OR** the `light_mode` capability.

Additionally, users can reorder selected devices via drag-and-drop.

<pre class="language-json" data-title="widget.compose.json"><code class="lang-json"><strong>{
</strong>  "name": {
    "en": "My Widget"
  },
  "devices": {
    "type": "global",
    "singular": false,
    "filter": {
      "class": "socket",
      "capabilities": "onoff"
    }
  },
  "settings": [
    {
      "id": "text",
      "type": "text",
      "title": {
        "en": "Text"
      },
      "hint": {
        "en": "Your number"
      }
    },
    {
      "id": "number",
      "type": "number",
      "title": {
        "en": "Number"
      },
      "hint": {
        "en": "Your number"
      }
    }
  ]
}
</code></pre>


# Styling

Styling your widget with the provided CSS variables and classes helps it fit nicely alongside other widgets on the user’s dashboard.

{% hint style="info" %}
Styling for widgets is not the same as for custom views. While there are some variables and classes with the same name, the actual styling might differ.
{% endhint %}

The provided CSS variables and classes are prefixed with homey, so `--homey` for variables and `.homey` for classes. We recommend using these CSS variables and classes as much as possible. Most widgets can be created using only these classes or by adding just a few lines of custom CSS. However, feel free to use them as a starting point and add custom styling to your liking.

## Widget

Your widget consists of a title, a frame, and the content you provide. While you have limited control over the title and frame, most of this style guide focuses on styling the content within your widget.

### Title

The default title of your widget is the name set in `widget.compose.json` (see [/pages/XyArR9HsPghISLXQ4wdP#widget.compose.json](https://apps.developer.homey.app/the-basics/widgets/pages/XyArR9HsPghISLXQ4wdP#widget.compose.json "mention")). When users add your widget to their dashboard, they can choose to either change the title or hide it entirely.

<figure><img src="/files/8g5kxfQFBgM36HAY4ex3" alt=""><figcaption><p>Empty widget with title (left) and without title (right).</p></figcaption></figure>

### Frame

Your widget’s content is placed inside a frame with rounded corners and a shadow. While you can't change the border radius, shadow or width of the frame, you do have control over the height (see [/pages/XyArR9HsPghISLXQ4wdP#widget.compose.json](https://apps.developer.homey.app/the-basics/widgets/pages/XyArR9HsPghISLXQ4wdP#widget.compose.json "mention")) and the background color.

Additionally, you can set the `transparent` property in [`widget.compose.json`](https://apps.developer.homey.app/the-basics/widgets/pages/XyArR9HsPghISLXQ4wdP#widget.compose.json), which allows for a fully transparent background. This option is ideal for creating seamless widgets that integrate smoothly without the default background color.

#### Background color

By default, the background color of your widget is set to `--homey-background-color`. This semantic color variable (see [#semantic](#semantic "mention")) can also be used for other elements in your widget.

<table><thead><tr><th width="318">CSS Variable</th><th>Value</th></tr></thead><tbody><tr><td><code>--homey-background-color</code></td><td><code>--homey-color-mono-000</code> (white) in light mode and <code>--color-mono-050</code> (dark grey) in dark mode (see <a data-mention href="#palette">#palette</a>).</td></tr></tbody></table>

You can apply a different background color to the body of your widget:

```html
<body class="homey-widget my-custom-body">
  <!-- Content of you widget here. -->
</body>
```

```css
.my-custom-body {
    background-color: var(--homey-color-blue);
}
```

## Spacing

When widgets are next to each other, it's visually pleasing to have the elements inside of the widgets aligned. In addition, the distances between elements inside of a widget make a huge difference in how organised, or cluttered, the dashboard feels.

### Space Units

To maintain consistent spacing across your widgets, we provide a set of predefined CSS variables based on a space unit, `--homey-su`. This unit serves as the foundation for all spacing-related styles. You can use these variables to apply consistent margins, padding, and other spacing properties to your widget elements.

We don't use space units for sizing properties, like width and height. The size of an element is often determined by other factors, like text length, or by the available space left after applying margins and paddings (e.g. `width: 100%;`).

The base space unit is set to 4px. Each derived variable is a multiple of the base space unit.

```css
--homey-su: 4px;                         /* base */
--homey-su-1: calc(var(--homey-su) * 1); /*  4px */
--homey-su-2: calc(var(--homey-su) * 2); /*  8px */
--homey-su-3: calc(var(--homey-su) * 3); /* 12px */
--homey-su-4: calc(var(--homey-su) * 4); /* 16px */
--homey-su-5: calc(var(--homey-su) * 5); /* 20px */
--homey-su-6: calc(var(--homey-su) * 6); /* 24px */
--homey-su-7: calc(var(--homey-su) * 7); /* 28px */
--homey-su-8: calc(var(--homey-su) * 8); /* 32px */
```

#### Usage Example

You can use these variables to ensure consistent spacing within your widgets. For example:

```css
.widget-item {
  margin-left: var(--homey-su-2);
  padding: var(--homey-su-4)
}
```

### Widget Padding

To align the elements on the edges of your the widget to other widgets, we offer the following classes:

<table><thead><tr><th width="318">CSS Class</th><th>Purpose</th></tr></thead><tbody><tr><td><code>.homey-widget</code></td><td>Default and recommended for most widgets. This class applies a padding of <code>--homey-su-4</code>.</td></tr><tr><td><code>.homey-widget-small</code></td><td>Used to save space in small widgets, this class applies a padding of <code>--homey-su-2</code>.</td></tr><tr><td><code>.homey-widget-full</code></td><td>Use this class, or omit the class entirely, for widgets where you need the full space (e.g. tables or images). The default padding for the element is 0.</td></tr></tbody></table>

#### Usage Examples

The space between your widget’s content and the edge of its frame affects how well it aligns with other widgets. To ensure a consistent look across the dashboard, we’ve added the `.homey-widget` class to the body of your widget by default. This class applies a padding of `--homey-su-4` (16px, see [#space-units](#space-units "mention")) to your widget:

```html
<body class="homey-widget">
  <!-- Content of you widget here. -->
</body>
```

For smaller widgets, this padding might take up more space than desired. In such cases, you can use the `.homey-widget-small` class, which sets the padding to `--homey-su-2` (8px):

```html
<body class="homey-widget-small">
  <!-- Content of you widget here. -->
</body>
```

For widgets that include elements like tables or images that need to extend to the edges of the frame, you can either remove the `.homey-widget` class or use the `.homey-widget-full` class, which sets the padding to 0. Alternatively, you can move the `.homey-widget` class to a child element for the rest of your content:

<pre class="language-html"><code class="lang-html"><strong>&#x3C;body class="homey-widget-full">
</strong>  &#x3C;!-- Full size element here. -->
  &#x3C;div class="homey-widget">
    &#x3C;!-- Content of you widget here. -->
  &#x3C;/div>
&#x3C;/body>
</code></pre>

## Text

Describing every possible combination of text properties would be too extensive. Instead, we offer several text presets as CSS classes that cover a wide range of use cases, along with CSS variables for font size, font weight, line height and color. Additionally, we provide classes for text alignment.

### Text Presets

The following CSS classes are a great starting point for styling your widget. They apply a font size smaller than the widget title, and their combination of font weight, line height, and color helps establish a clear visual hierarchy within your widget.

<table><thead><tr><th width="318">CSS Class</th><th>Purpose</th></tr></thead><tbody><tr><td><code>.homey-text-bold</code></td><td>Used for titles or a singular important text.</td></tr><tr><td><code>.homey-text-medium</code></td><td>Used to make text stand out, for strong text or subtitles.</td></tr><tr><td><code>.homey-text-regular</code></td><td>Default for most text.</td></tr><tr><td><code>.homey-text-small</code></td><td>Used for small text on it's own.</td></tr><tr><td><code>.homey-text-small-light</code></td><td>Used for small text next to other texts.</td></tr></tbody></table>

In the image below are examples of when to use these classes.

<figure><img src="/files/WMRjLMoqoUdg8Rvlsn94" alt=""><figcaption><p>Example usage of CSS classes for text in timeline widget (left) and lights widget (right).</p></figcaption></figure>

#### Usage Example

<pre class="language-html"><code class="lang-html"><strong>&#x3C;h1 class="homey-text-bold">Hello World!&#x3C;/h1>
</strong><strong>&#x3C;p class="homey-text-regular">How are you?&#x3C;/p>
</strong></code></pre>

### Variables

We provide CSS variables for font size, font weight, line height, and color. To maintain consistency, all text on the dashboard follows specific combinations of font sizes and font weights. When using the provided CSS variables, refer to the table below for the recommended combinations. Additionally, ensure that the line height you choose matches the corresponding font size (see [#line-height](#line-height "mention")).

#### Table of combinations for font size and font weight

<table><thead><tr><th align="center"></th><th data-type="checkbox">regular</th><th data-type="checkbox">medium</th><th data-type="checkbox">bold</th></tr></thead><tbody><tr><td align="center">xxlarge</td><td>false</td><td>false</td><td>true</td></tr><tr><td align="center">xlarge</td><td>false</td><td>false</td><td>true</td></tr><tr><td align="center">large</td><td>false</td><td>true</td><td>false</td></tr><tr><td align="center">default</td><td>true</td><td>true</td><td>true</td></tr><tr><td align="center">small</td><td>true</td><td>false</td><td>false</td></tr></tbody></table>

The image below shows several combinations used in widgets.

<figure><img src="/files/ikhezSrRW1ml8TcJcMUj" alt=""><figcaption><p>Example usage of CSS variables for text in weather widget (left) and battery widget (right).</p></figcaption></figure>

#### Font Size

It is recommended to use text sizes smaller than the title above the widgets, which is 20px. The default text size `--homey-font-size-default` is 17px.

We provide the following variables:

<table><thead><tr><th width="318">CSS Variable</th><th width="84">Value</th><th>Purpose</th></tr></thead><tbody><tr><td><code>--homey-font-size-xxlarge</code></td><td>32px</td><td>Used for numbers only.</td></tr><tr><td><code>--homey-font-size-xlarge</code></td><td>24px</td><td>Use sparingly, only for short phrases or single words that really have to stand out.</td></tr><tr><td><code>--homey-font-size-large</code></td><td>20px</td><td>Used for numbers only.</td></tr><tr><td><code>--homey-font-size-default</code></td><td>17px</td><td>Default for most text.</td></tr><tr><td><code>--homey-font-size-small</code></td><td>14px</td><td>For captions, tables, underneath other text, or inside specific elements.</td></tr></tbody></table>

#### Line Height

We have a specific line height for every font size. It is highly recommended to always use these together.

<table><thead><tr><th width="318">CSS Variable</th><th width="84">Value</th><th>Use with font size</th></tr></thead><tbody><tr><td><code>--homey-line-height-xxlarge</code></td><td>40px</td><td><code>--homey-font-size-xxlarge</code></td></tr><tr><td><code>--homey-line-height-xlarge</code></td><td>32px</td><td><code>--homey-font-size-xlarge</code></td></tr><tr><td><code>--homey-line-height-large</code></td><td>28px</td><td><code>--homey-font-size-large</code></td></tr><tr><td><code>--homey-line-height-default</code></td><td>24px</td><td><code>--homey-font-size-default</code></td></tr><tr><td><code>--homey-line-height-small</code></td><td>20px</td><td><code>--homey-font-size-small</code></td></tr></tbody></table>

#### Font Weight

Important text, such as titles, can be made to stand out by adjusting the font weight. Refer to the table above (see [#table-of-combinations-for-font-size-and-font-weight](#table-of-combinations-for-font-size-and-font-weight "mention")) to find the appropriate font weight for your chosen font size.

<table><thead><tr><th width="318">CSS Variable</th><th width="85" data-type="number">Value</th><th>Purpose</th></tr></thead><tbody><tr><td><code>--homey-font-weight-bold</code></td><td>700</td><td>Used for titles.</td></tr><tr><td><code>--homey-font-weight-medium</code></td><td>500</td><td>Used to make text stand out, for strong text or subtitles.</td></tr><tr><td><code>--homey-font-weight-regular</code></td><td>400</td><td>Default for most text.</td></tr></tbody></table>

#### Text Color

The default text color is `--homey-text-color`. This color depends on if the widget is shown in light or dark mode (see [#light-and-dark-mode](#light-and-dark-mode "mention")) and fits nicely on a background with `--homey-background-color` (see [#background-color](#background-color "mention")).

For use on different backgrounds or different purposes, we offer the following semantic color variables (see [#semantic](#semantic "mention")).

| CSS Variable                   | Purpose                                                                                      |
| ------------------------------ | -------------------------------------------------------------------------------------------- |
| `--homey-text-color`           | Default text color.                                                                          |
| `--homey-text-color-light`     | Used for text that's less important, or disabled.                                            |
| `--homey-text-color-white`     | White text, independent of light or dark mode. Used for text on dark or colored backgrounds. |
| `--homey-text-color-blue`      | Blue text.                                                                                   |
| `--homey-text-color-green`     | Green text.                                                                                  |
| `--homey-text-color-orange`    | Orange text.                                                                                 |
| `--homey-text-color-red`       | Red text.                                                                                    |
| `--homey-text-color-highlight` | Text to highlight something.                                                                 |
| `--homey-text-color-success`   | Text for success case.                                                                       |
| `--homey-text-color-warning`   | Text for warnings.                                                                           |
| `--homey-text-color-danger`    | Text for errors.                                                                             |

#### Usage Example

```html
<p class="my-custom-text">HELP ME!</p>
```

```css
.my-custom-text {
    font-size: var(--homey-font-size-medium);
    font-weight: var(--homey-font-weight-bold);
    line-height: var(--homey-line-height-medium); /* Matches with font-size. */
    color: var(--homey-text-color-danger);
}
```

### Text Alignment

We have the following CSS classes to change the text alignment to left, center or right.

<table><thead><tr><th width="318">CSS Class</th><th>Purpose</th></tr></thead><tbody><tr><td><code>.homey-text-align-left</code></td><td>Align text left.</td></tr><tr><td><code>.homey-text-align-center</code></td><td>Align text center.</td></tr><tr><td><code>.homey-text-align-right</code></td><td>Align text right.</td></tr></tbody></table>

## Colors

We provide a color palette that includes grayscale, blues, greens, orange, and reds, in addition to semantic colors for specific purposes. Our palette supports both light and dark mode, with each CSS variable automatically adjusting to the active mode. However, the actual color may differ between modes.

### Light & Dark Mode

The dashboard switches between light and dark mode based on the user’s settings. By default, the widget background is white in light mode and dark grey in dark mode.

If you want your widget to always appear in dark mode, regardless of the user's settings, you can add the `.homey-dark-mode` class to the element. This forces the widget to stay in dark mode.

<table><thead><tr><th width="318">CSS Class</th><th>Purpose</th></tr></thead><tbody><tr><td><code>.homey-dark-mode</code></td><td>Force widget to dark mode independent of user settings.</td></tr></tbody></table>

#### Usage Example

```html
<body class="homey-widget homey-dark-mode">
  <!-- Content of you widget here. -->
</body>
```

If you want to check if darkmode is enabled you can use the selector `.homey-dark-mode my-selector`.

### Palette

Our color palette includes grayscale, blues, greens, orange, and reds. While you can use these CSS variables directly, we recommend using the semantic color variables where possible (see [#semantic](#semantic "mention")).

The grayscale is prefixed with `--homey-color-mono` and ranges from `--homey-color-mono-000`(white in light mode) to `--homey-color-mono-1000` (black in light mode).

<figure><img src="/files/YbjJjqymUgX0Z5t545L3" alt=""><figcaption><p>Color palette grayscale in light mode (left) and dark mode (right).</p></figcaption></figure>

The blue, green, orange and red colors are prefixed as `--homey-color-blue`, `--homey-color-green`, `--homey-color-orange` and `--homey-color-red` respectively. These colors range from `050` to `900`, except for orange, which only has the `500` value available. These colors remain the same in both light and dark mode.

<figure><img src="/files/CKXcJ5gZpKKWk6i2WVeo" alt=""><figcaption><p>Color palette for blue (top-left), green (bottom-left), orange (top-right) and red (bottom-right).</p></figcaption></figure>

### Semantic

To help you select the right color from our palette, we provide CSS variables for specific purposes, known as semantic color variables. These variables are defined using the palette CSS variables above ( see [#palette](#palette "mention")). So no new colors are introduced—just clarification on their intended use cases. This ensures consistent color usage across your widgets while keeping the palette simple and easy to apply.

<table><thead><tr><th width="318">CSS Variable</th><th>Purpose</th></tr></thead><tbody><tr><td><code>--homey-color-white</code></td><td>White color independent of light or dark mode.</td></tr><tr><td><code>--homey-color-blue</code></td><td>General purpose blue color.</td></tr><tr><td><code>--homey-color-green</code></td><td>General purpose green color.</td></tr><tr><td><code>--homey-color-orange</code></td><td>General purpose orange color.</td></tr><tr><td><code>--homey-color-red</code></td><td>General purpose red color.</td></tr><tr><td><code>--homey-color-highlight</code></td><td>Highlight.</td></tr><tr><td><code>--homey-color-success</code></td><td>Success.</td></tr><tr><td><code>--homey-color-warning</code></td><td>Warning.</td></tr><tr><td><code>--homey-color-danger</code></td><td>Danger.</td></tr></tbody></table>

The semantic CSS variables for [#background-color](#background-color "mention"), [#text-color](#text-color "mention"), [#lines-and-borders](#lines-and-borders "mention") and [#icons](#icons "mention") can be found in their respective sections.

## Lines & Borders

Because the widget’s frame already has a shadow, we recommend avoiding additional shadows on elements inside your widget, as shadows within shadows can quickly become cluttered. Instead, we offer several CSS variables and classes to help you apply clean and consistent lines and borders to your elements.

### Borders

<table><thead><tr><th width="318">CSS Class</th><th>Purpose</th></tr></thead><tbody><tr><td><code>.homey-border</code></td><td>Adds a border to all sides of the element.</td></tr><tr><td><code>.homey-border-top</code></td><td>Add a border to the top of the element.</td></tr><tr><td><code>.homey-border-right</code></td><td>Add a border to the right side of the element.</td></tr><tr><td><code>.homey-border-bottom</code></td><td>Add a border to the bottom of the element.</td></tr><tr><td><code>.homey-border-left</code></td><td>Add a border to the left side of the element.</td></tr></tbody></table>

### Lines

The CSS classes for borders use the following CSS variables. These CSS variables can also be applied to other lines within your widget.

We provide two semantic color variables for line colors (see [#semantic](#semantic "mention")):

<table><thead><tr><th width="318">CSS Variable</th><th>Purpose</th></tr></thead><tbody><tr><td><code>--homey-line-color</code></td><td>Used for most lines. The default line color.</td></tr><tr><td><code>--homey-line-color-light</code></td><td>Used for light lines that should stand out less.</td></tr></tbody></table>

In almost all cases, we use a 1px solid line with the line colors mentioned above. You can use the following CSS variables to apply consistent lines like these to your widget elements:

<table><thead><tr><th width="318">CSS Variable</th><th>Purpose</th></tr></thead><tbody><tr><td><code>--homey-line</code></td><td>Used for most lines.</td></tr><tr><td><code>--homey-line-light</code></td><td>Used for light lines that should stand out less.</td></tr></tbody></table>

### Border Radius

We offer the following CSS variables for applying a border radius to your elements:

<table><thead><tr><th width="318">CSS Variable</th><th>Purpose</th></tr></thead><tbody><tr><td><code>--homey-border-radius-small</code></td><td>Only use where the default border radius is too big.</td></tr><tr><td><code>--homey-border-radius-default</code></td><td>Default border radius.</td></tr></tbody></table>

#### Usage Example

```html
<div class="my-custom-item"></div>
```

```css
.my-custom-item {
    border: var(--homey-line-light);
    border-radius: var(--homey-border-radius-default);
}
```

## Icons

For our icons, we always use SVGs and recommend you do the same for your custom icons, as SVGs ensure sharp display at any size. You can place your custom icons, along with other assets, in your public folder (see [/pages/XyArR9HsPghISLXQ4wdP#index.html](https://apps.developer.homey.app/the-basics/widgets/pages/XyArR9HsPghISLXQ4wdP#index.html "mention")).

### Custom Icons

If you want to add a custom icon, you can extend the .homey-custom-icon- class. Simply add a class starting with `.homey-custom-icon-` (e.g., `.homey-custom-icon-example`) to your element. In your CSS, use your SVG as the `mask-image` and include `-webkit-mask-image` for older browser support.

#### Usage Example

```html
<div class="homey-custom-icon-example"></div>
```

```css
.homey-custom-icon-example {
    -webkit-mask-image: url('example.svg'); /* Browser support. */
    mask-image: url('example.svg');
}
```

### Variables

We have the following semantic color variables for icon colors (see [#semantic](#semantic "mention")):

<table><thead><tr><th width="318">CSS Variable</th><th>Purpose</th></tr></thead><tbody><tr><td><code>--homey-icon-color-dark</code></td><td>Default icon color.</td></tr><tr><td><code>--homey-icon-color-light</code></td><td>Used less important icons or disabled states.</td></tr><tr><td><code>--homey-icon-color-white</code></td><td>White icons, independent of light or dark mode.</td></tr><tr><td><code>--homey-icon-color-blue</code></td><td>Blue icons.</td></tr><tr><td><code>--homey-icon-color-green</code></td><td>Green icons.</td></tr><tr><td><code>--homey-icon-color-orange</code></td><td>Orange icons.</td></tr><tr><td><code>--homey-icon-color-red</code></td><td>Red icons.</td></tr></tbody></table>

We have the following CSS variable for icon sizing:

<table><thead><tr><th width="318">CSS Variable</th><th width="109">Value</th><th>Purpose</th></tr></thead><tbody><tr><td><code>--homey-icon-size-medium</code></td><td>20px</td><td>Default icon size.</td></tr><tr><td><code>--homey-icon-size-regular</code></td><td>16px</td><td>Used in line with regular text.</td></tr><tr><td><code>--homey-icon-size-small</code></td><td>14px</td><td>Used in line with small text.</td></tr></tbody></table>

#### Usage Example

```html
<div class="homey-custom-icon-example"></div>
```

```css
.homey-custom-icon-example {
    --homey-icon-color: var(--homey-icon-color-green);
    --homey-icon-size: var(--homey-icon-size-small);

    -webkit-mask-image: url('example.svg');
    mask-image: url('example.svg');
}
```

## Tables

We offer two different table styles: tables with lines between cells, using the `.homey-table` class, and tables with striped rows, using the `.homey-table-striped` class. We recommend using the `.homey-table-striped` class only for tables with a few columns or when the content has enough horizontal spacing that dividing lines aren’t necessary.

<table><thead><tr><th width="318">CSS Class</th><th>Purpose</th></tr></thead><tbody><tr><td><code>.homey-table</code></td><td>Default table styling.</td></tr><tr><td><code>.homey-table-striped</code></td><td>Used for tables with only a few columns.</td></tr></tbody></table>

<figure><img src="/files/TgsXsmvf1zKPA1SYcBaw" alt=""><figcaption><p>Example of CSS classes .homey-table (left) and .homey-table-striped (right).</p></figcaption></figure>

To change the text alignment in the cells of the table, you can apply the text alignment classes (see [#text-alignment](#text-alignment "mention")) to the table element.

#### Usage Example

```html
    <table class="homey-table text-align-center">
      <thead>
      <tr>
        <th>Header 1</th>
        <th>Header 2</th>
        <th>Header 3</th>
      </tr>
      </thead>
      <tbody>
      <tr>
        <td>Row 1, Cell 1</td>
        <td>Row 1, Cell 2</td>
        <td>Row 1, Cell 3</td>
      </tr>
      <tr>
        <td>Row 2, Cell 1</td>
        <td>Row 2, Cell 2</td>
        <td>Row 2, Cell 3</td>
      </tr>
      <tr>
        <td>Row 3, Cell 1</td>
        <td>Row 3, Cell 2</td>
        <td>Row 3, Cell 3</td>
      </tr>
      </tbody>
    </table>
```


# Debugging

Since the widget runs within a webview in the mobile app, you can use the Chrome inspector on Android or Safari Web Inspector on iOS to debug. This allows you to view the console and inspect the HTML of the widget.

## Android Debugging

### Mobile

1. Plug in your mobile device.
2. Ensure USB debugging is enabled:\
   **Settings** → **System** → **Developer options** → enable **USB debugging**.

### PC

1. Open **Chrome** and navigate to `chrome://inspect/#devices` (or on **Edge**, use `edge://inspect/#devices`).
2. Under **Remote Target**, look for **"WebView in app.homey."**
3. Click **Inspect**.

## iOS Debugging

### Mobile

1. Plug in your mobile device.
2. Ensure USB debugging is enabled:\
   **Settings** → **System** → **About Phone** → **Developer options** → enable **USB debugging**.

### PC

1. Open **Safari** Settings, go to the **Advanced** tab, and enable the checkbox **"Show Develop menu in menu bar."**
2. In **Safari** , go to **Develop** → **\[device name]** → **\[app name]** → **\[url - title]**.

<figure><img src="/files/T2dVeHHYHdlfSW1ik8oV" alt=""><figcaption><p>Chrome Web Inspector</p></figcaption></figure>


# Wi-Fi

Homey connects over 2.4 GHz to your Wi-Fi network, and your app can access devices on the LAN.

Your app can access the local Wi-Fi network to connect with devices, and reach the internet to talk with external APIs.

{% hint style="info" %}
The Wi-Fi connection might not always be available. Homey will still function normally without Wi-Fi or internet, so ensure your app handles these cases.
{% endhint %}

## LAN

Homey can access devices on the LAN, for example on `192.168.1.100`. No extra permissions are required to access these devices locally.

### Discovery

The preferred way of connecting to LAN devices is by discovering them automatically. Learn more about connecting to LAN devices in the [Discovery guide](/wireless/wi-fi/discovery).

{% hint style="info" %}
Homey Bridge does not support local Wi-Fi connections, and therefore mDNS, SSDP and MAC discovery are not supported on Homey Cloud.
{% endhint %}

{% hint style="warning" %}
App Store submissions where users must enter an IP address, where discovery could have been used instead, will be rejected.
{% endhint %}

## Cloud

Homey can talk to the internet without extra permissions. Many Web APIs can be accessed this way.

### OAuth2

Many modern Web APIs authenticate using OAuth2, you can lean more about creating Homey apps that connect to OAuth2 Web APIs by reading the[ OAuth2 guide](/cloud/oauth2).

### Webhooks

Homey can receive external webhooks, even if it's behind a user's router. You can learn more about receiving webhook events in Homey apps by reading the [webhooks guide](/cloud/webhooks).


# Discovery

Discover LAN devices using mDNS-SD, SSDP or Manufacturer's MAC address (ARP).

Homey can automatically find devices on the user's Wi-Fi network using mDNS-SD, SSDP and MAC. This provides the best experience for a user, because entering an IP address —which can even change over time— is not a great experience.

{% hint style="info" %}
Homey Bridge does not support local Wi-Fi connections, and therefore mDNS, SSDP and MAC discovery are not supported on Homey Cloud.
{% endhint %}

As a bonus, when using Discovery in conjunction with a Driver, Homey manages the Device's availability state automatically.

{% hint style="info" %}
You can view a working example of a Homey App that uses Discovery at: <https://github.com/athombv/com.plugwise.adam-example>
{% endhint %}

## Choosing a discovery strategy

Devices can be discovered using different strategies but most devices use mDNS-SD. To list mDNS-SD devices in your network you can use [Discovery](https://apps.apple.com/us/app/discovery-dns-sd-browser/id305441017) on MacOS or [Bonjour browser](https://hobbyistsoftware.com/bonjourbrowser) on Windows. If a device is not discoverable with mDNS-SD please refer to your device's documentation to learn what strategy you can use for discovery.

### mDNS-SD

[Multicast-DNS Service Discovery](https://en.wikipedia.org/wiki/Multicast_DNS) is a widely used protocol to find devices on a network. It is also known as Avahi or Bonjour. A device broadcasts its presence under a `name` (as specified by the manufacturer) and a `protocol` (`tcp` or `udp`). For example, Homey broadcasts its presence with the name `homey` using the `tcp` protocol.

A `DiscoveryResultMDNSSD` has a `txt` property that contains the (lowercased) TXT values of the broadcast.

{% code title="/.homeycompose/discovery/nanoleaf-aurora.json" %}

```javascript
{
  "type": "mdns-sd",
  "mdns-sd": {
    "name": "nanoleafapi",
    "protocol": "tcp"
  },
  "id": "{{txt.id}}",
  "conditions": [
    [
      {
        "field": "txt.md",
        "match": {
          "type": "string",
          "value": "NL22"
        }
      }
    ]
  ]
}
```

{% endcode %}

{% hint style="info" %}
The discovery `conditions` are optional but highly recommended, you can use these to pre-filter the discovery results. This way your app only receives the discovery results of devices that can actually be paired using your app.
{% endhint %}

### SSDP

Devices using the [Simple Service Discovery Protocol](https://en.wikipedia.org/wiki/Simple_Service_Discovery_Protocol) can be found by specifying a `search` property.

A `DiscoveryResultSSDP` has a `headers` property that contains the (lowercased) headers of the response.

{% code title="/.homeycompose/discovery/denon-heos.json" %}

```javascript
{
  "type": "ssdp",
  "ssdp": {
    "search": "urn:schemas-denon-com:device:ACT-Denon:1"
  },
  "id": "{{headers.usn}}",
  "conditions": [
    [
      {
        "field": "headers.st",
        "match": {
          "type": "string",
          "value": "urn:schemas-denon-com:device:ACT-Denon:1"
        }
      }
    ]
  ]
}
```

{% endcode %}

### MAC

MAC Address discovery works by specifying the first 3 bytes of a network device's MAC address. These first three bytes are reserved for the manufacturer and can thus be used to find a device on the network using Address Resolution Protocol (ARP).

A `DiscoveryResultMAC` only has an `address` property, which contains the IP address of the device.

For example, to find a device with a MAC address that starts with `00:24:6d` or `00:24:6e`, convert them from hexadecimal to decimal.

{% code title="/.homeycompose/discovery/weinzierl.json" %}

```javascript
{
  "type": "mac",
  "mac": {
    "manufacturer": [
      [ 0, 36, 109 ],
      [ 0, 36, 110 ]
    ]
  }
}
```

{% endcode %}

{% hint style="info" %}
The MAC address must be specified in decimal numbers, because JSON does not support hexadecimal-notation.
{% endhint %}

## Defining your discovery strategy

### The Discovery Result

Depending on your discovery type, some properties are available to match on. For example, an mDNS-SD discovery type has a `txt` object and the SSDP discovery type has an `headers` object. The discovery result provides the address that you can use to connect to the device.

### Discovery Result ID

For the `mdns-sd` and `ssdp` discovery types, the app must define how a discovery result can be identified when it has been found multiple times, regardless if the IP address has changed.

For the `mac` discovery type, the mac address is used as the ID.

Find a unique and consistent property in the discovery result and define it as `id` in the App Manifest. Homey will then be able to match the result to previous results, and notify your app the device has been found again, instead of seeing the device as a new discovery result.

**Examples:**

```javascript
"id": "{{txt.id}}"
```

```javascript
"id": "{{headers.uuid}}"
```

All properties available in the DiscoveryResult are available between double curly braces (`{{` and `}}`).

### Discovery Result Conditions

A discovery strategy can have a set of conditions that must be true before the result is sent to the app.

The `conditions` property is an `Array` with one or more `Arrays` in it. This array contains `Objects` rules. When all rules within an array are true, the result is considered a match. Using multiple arrays behaves as `rulesArray1 OR rulesArray2 OR ...`.

There are two match types available: `string` and `regex`.

```javascript
"conditions": [
  [
    {
      "field": "txt.md",
      "match": {
        "type": "string",
        "value": "NL29"
      }
    },
    // AND:
    {
      "field": "txt.version",
      "match": {
        "type": "string",
        "value": "1"
      }
    }
  ],
  // OR:
  [
    {
      "field": "txt.md",
      "match": {
        "type": "regex",
        "value": "NL\\d\\d" // double slashes because of JSON
      }
    }
  ]
]
```

{% hint style="info" %}
Conditions are matched case-insensitive.
{% endhint %}

## Using discovery with a Driver

The recommended way to use Homey's built-in discovery is to link a discovery strategy to a Driver. If you use a discovery strategy with a Driver Homey will automatically manage the availability of your Devices. You can then use the `onDiscovery*` methods in your Device class to get updated whenever the status of the device changes.

To start using discovery with a driver, add the `discovery` property to your driver's entry in the App Manifest.

For example:

{% code title="/.homeycompose/discovery/my\_discovery.json" %}

```javascript
{
  "type": "mdns-sd",
  "mdns-sd": {
    "protocol": "tcp",
    "name": "my_service"
  }
}
```

{% endcode %}

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
  "discovery": "my_discovery"
```

{% endcode %}

{% tabs %}
{% tab title="JavaScript" %}
In your `driver.js`, you can call [`Driver#getDiscoveryStrategy()`](https://apps-sdk-v3.developer.homey.app/Driver.html#getDiscoveryStrategy) to get the current strategy.

{% code title="/drivers/\<driver\_id>/driver.js" %}

```javascript
const Homey = require('homey');

class Driver extends Homey.Driver {
  async onPairListDevices() {
    const discoveryStrategy = this.getDiscoveryStrategy();
    const discoveryResults = discoveryStrategy.getDiscoveryResults();

    const devices = Object.values(discoveryResults).map(discoveryResult => {
      return {
        name: discoveryResult.txt.name,
        data: {
          id: discoveryResult.id,
        },
      };
    });

    return devices;
  }
}

module.exports = Driver;
```

{% endcode %}

In your `device.js`, overload the methods starting with `onDiscovery`.

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require('homey');

class Device extends Homey.Device {
  onDiscoveryResult(discoveryResult) {
    // Return a truthy value here if the discovery result matches your device.
    return discoveryResult.id === this.getData().id;
  }

  async onDiscoveryAvailable(discoveryResult) {
    // This method will be executed once when the device has been found (onDiscoveryResult returned true)
    this.api = new MyDeviceAPI(discoveryResult.address);
    await this.api.connect(); // When this throws, the device will become unavailable.
  }

  onDiscoveryAddressChanged(discoveryResult) {
    // Update your connection details here, reconnect when the device is offline
    this.api.address = discoveryResult.address;
    this.api.reconnect().catch(this.error); 
  }

  onDiscoveryLastSeenChanged(discoveryResult) {
    // When the device is offline, try to reconnect here
    this.api.reconnect().catch(this.error); 
  }
}

module.exports = Device;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
In your `driver.mts`, you can call [`Driver#getDiscoveryStrategy()`](https://apps-sdk-v3.developer.homey.app/Driver.html#getDiscoveryStrategy) to get the current strategy.

{% code title="/drivers/\<driver\_id>/driver.mts" %}

```mts
import Homey, { type DiscoveryResultMDNSSD } from "homey";

export default class Driver extends Homey.Driver {
  async onPairListDevices(): Promise<object[]> {
    const discoveryStrategy = this.getDiscoveryStrategy();
    const dicoveryResults = discoveryStrategy.getDiscoveryResults() as { [id: string]: DiscoveryResultMDNSSD };

    const devices = Object.values(dicoveryResults).map(discoveryResult => ({
      name: discoveryResult.name,
      data: { id: discoveryResult.id },
    }));

    return devices;
  }
}

```

{% endcode %}

In your `device.mts`, overload the methods starting with `onDiscovery`.

{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey, { type DiscoveryResultMDNSSD } from "homey";
import MyDeviceApi from "device-api";

export default class Device extends Homey.Device {
  api?: MyDeviceApi;

  onDiscoveryResult(discoveryResult: DiscoveryResultMDNSSD): boolean {
    // Return a truthy value here if the discovery result matches your device.
    return discoveryResult.id === this.getData().id;
  }

  async onDiscoveryAvailable(discoveryResult: DiscoveryResultMDNSSD): Promise<void> {
    // This method is called when a discovery result matching this device is found, in order to set up a connection with the device.
    this.api = new MyDeviceApi(discoveryResult.address);
    await this.api.reconnect(); // Throwing an exception here will make the device unavailable with the exception's message.
  }

  onDiscoveryAddressChanged(discoveryResult: DiscoveryResultMDNSSD): void {
    // This method is called when the device was found again at a different address.
    if (this.api === undefined) return;
    this.api.address = discoveryResult.address;
    // Reconnect in case the device was offline
    this.api.reconnect().catch(this.error);
  }

  onDiscoveryLastSeenChanged(discoveryResult: DiscoveryResultMDNSSD): void {
    // This method is called when the device has been found again.
    if (this.api === undefined) return;
    // Reconnect in case the device was offline
    this.api.reconnect().catch(this.error);
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
In your `driver.py`, you can call [`Driver#get_discovery_strategy()`](https://python-apps-sdk-v3.developer.homey.app/driver.html#homey.driver.Driver.get_discovery_strategy) to get the current strategy.

{% code title="/drivers/\<driver\_id>/driver.py" %}

```python
import typing

from homey import driver
from homey.discovery_result_mdns_sd import DiscoveryResultMDNSSD
from homey.discovery_strategy import DiscoveryStrategy
from homey.driver import ListDeviceProperties


class Driver(driver.Driver):
    async def on_pair_list_devices(self, view_data: dict) -> list[ListDeviceProperties]:
        discovery_strategy = typing.cast(
            DiscoveryStrategy[DiscoveryResultMDNSSD], self.get_discovery_strategy()
        )
        discovery_results = discovery_strategy.get_discovery_results()
        return [
            {
                "name": discovery_result.name or "Device",
                "data": {"id": discovery_result.id},
            }
            for discovery_result in discovery_results.values()
        ]


homey_export = Driver

```

{% endcode %}

In your `device.py`, overload the methods starting with `onDiscovery`.

{% code title="/drivers/\<driver\_id>/device.py" %}

```python
from typing import cast

from device_api import MyDeviceApi
from homey import device
from homey.discovery_result import DiscoveryResult
from homey.discovery_result_mdns_sd import DiscoveryResultMDNSSD


class Device(device.Device):
    api: MyDeviceApi | None

    async def on_discovery_result(self, discovery_result: DiscoveryResult) -> bool:
        # Return a truthy value here if the discovery result matches your device.
        return discovery_result.id == self.get_data().get("id")

    async def on_discovery_available(self, discovery_result: DiscoveryResult) -> None:
        # This method is called when a discovery result matching this device is found, in order to set up a connection with the device.
        address = cast(DiscoveryResultMDNSSD, discovery_result).address
        if address is None:
            return
        self.api = MyDeviceApi(address)
        await self.api.reconnect()

    async def on_discovery_address_changed(
        self, discovery_result: DiscoveryResult
    ) -> None:
        # This method is called when the device was found again at a different address.
        if self.api is None or discovery_result.address is None:
            return
        self.api.address = discovery_result.address
        try:
            # Reconnect in case the device was offline
            await self.api.reconnect()
        except Exception as e:
            self.error(e)

    async def on_discovery_last_seen_changed(self, discovery_result: DiscoveryResult):
        # This method is called when the device has been found again.
        if self.api is None:
            return
        try:
            # Reconnect in case the device was offline
            await self.api.reconnect()
        except Exception as e:
            self.error(e)


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

## Using discovery standalone

{% tabs %}
{% tab title="JavaScript" %}
Simply call [`ManagerDiscovery#getStrategy()`](https://apps-sdk-v3.developer.homey.app/ManagerDiscovery.html#getStrategy) with the discovery strategy ID as defined in your App Manifest. You can then call [`DiscoveryStrategy#getDiscoveryResults()`](https://apps-sdk-v3.developer.homey.app/DiscoveryStrategy.html#getDiscoveryResults) to get the devices that already have been discovered and listen to the `result` event on the `DiscoveryStrategy` to react to newly discovered devices while the app is running.

{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  async onInit() {
    const discoveryStrategy = this.homey.discovery.getStrategy("my_strategy");

    // Use the discovery results that were already found
    const initialDiscoveryResults = discoveryStrategy.getDiscoveryResults();
    for (const discoveryResult of Object.values(initialDiscoveryResults)) {
      this.handleDiscoveryResult(discoveryResult);
    }

    // And listen to new results while the app is running
    discoveryStrategy.on("result", discoveryResult => {
      this.handleDiscoveryResult(discoveryResult);
    });
  }

  handleDiscoveryResult(discoveryResult) {
    this.log("Got result:", discoveryResult);
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
Simply call [`ManagerDiscovery#getStrategy()`](https://apps-sdk-v3.developer.homey.app/ManagerDiscovery.html#getStrategy) with the discovery strategy ID as defined in your App Manifest. You can then call [`DiscoveryStrategy#getDiscoveryResults()`](https://apps-sdk-v3.developer.homey.app/DiscoveryStrategy.html#getDiscoveryResults) to get the devices that already have been discovered and listen to the `result` event on the `DiscoveryStrategy` to react to newly discovered devices while the app is running.

{% code title="/app.mts" %}

```mts
import Homey, { type DiscoveryResultMAC } from "homey";

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    const discoveryStrategy = this.homey.discovery.getStrategy("my_strategy");

    // Use the discovery results that were already found
    const initialDiscoveryResults = discoveryStrategy.getDiscoveryResults() as { [id: string]: DiscoveryResultMAC };
    for (const discoveryResult of Object.values(initialDiscoveryResults)) {
      this.handleDiscoveryResult(discoveryResult);
    }

    // And listen to new results while the app is running
    discoveryStrategy.on("result", (discoveryResult: DiscoveryResultMAC) => {
      this.handleDiscoveryResult(discoveryResult);
    });
  }

  handleDiscoveryResult(discoveryResult: DiscoveryResultMAC): void {
    this.log("Got result:", discoveryResult);
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
Simply call [`ManagerDiscovery#get_strategy()`](https://python-apps-sdk-v3.developer.homey.app/manager/discovery.html#homey.manager.discovery.ManagerDiscovery.get_strategy) with the discovery strategy ID as defined in your App Manifest. You can then call [`DiscoveryStrategy#get_discovery_results()`](https://python-apps-sdk-v3.developer.homey.app/discovery_strategy.html#homey.discovery_strategy.DiscoveryStrategy.get_discovery_results) to get the devices that already have been discovered and listen to the `result` event on the `DiscoveryStrategy` to react to newly discovered devices while the app is running.

{% code title="/app.py" %}

```python
from typing import cast

from homey import app
from homey.discovery_result_mac import DiscoveryResultMAC


class App(app.App):
    async def on_init(self) -> None:
        discovery_strategy = self.homey.discovery.get_strategy("my_strategy")

        initial_discovery_results = discovery_strategy.get_discovery_results()
        for discovery_result in initial_discovery_results.values():
            self.handle_discovery_result(cast(DiscoveryResultMAC, discovery_result))

        discovery_strategy.on("result", self.handle_discovery_result)

    def handle_discovery_result(self, discovery_result: DiscoveryResultMAC):
        self.log("Got result:", discovery_result)


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}


# 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.

{% hint style="info" %}
In order to use BLE in your app will need the `homey:wireless:ble` permission. For more information about permissions read the [permissions guide](/the-basics/app/permissions).
{% endhint %}

{% hint style="info" %}
You can view a working example of a Homey App that uses BLE at: <https://github.com/athombv/com.mipow-example>
{% endhint %}

## Device discovery

{% tabs %}
{% tab title="JavaScript" %}
One of the first things to do when using Bluetooth on Homey is to perform a *device* *discovery.* Using the [`ManagerBLE#discover()`](https://apps-sdk-v3.developer.homey.app/ManagerBLE.html#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.

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

{% endtab %}

{% tab title="TypeScript" %}
One of the first things to do when using Bluetooth on Homey is to perform a *device* *discovery.* Using the [`ManagerBLE#discover()`](https://apps-sdk-v3.developer.homey.app/ManagerBLE.html#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.

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

{% endtab %}

{% tab title="Python" %}
One of the first things to do when using Bluetooth on Homey is to perform a *device* *discovery.* Using the [`ManagerBLE#discover()`](https://python-apps-sdk-v3.developer.homey.app/manager/ble.html#homey.manager.ble.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.

```python
advertisements = await self.homey.ble.discover()
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
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.
{% endhint %}

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[]` | <p>Some peripherals show one or more services in their advertisement.</p><p>This list does not necessarily contain <em>all</em> services of this peripheral.</p> | 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.

{% tabs %}
{% tab title="JavaScript" %}

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

{% endtab %}

{% tab title="TypeScript" %}

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

{% endtab %}

{% tab title="Python" %}

```python
advertisements = await self.homey.ble.discover(
    ["0000180000001000800000805F9B34FB"]
)
```

{% endtab %}
{% endtabs %}

### Finding devices by UUID using find()

{% tabs %}
{% tab title="JavaScript" %}
An alternative to the `discover()` function is the [`ManagerBLE#find()`](https://apps-sdk-v3.developer.homey.app/ManagerBLE.html#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:

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

{% endtab %}

{% tab title="TypeScript" %}
An alternative to the `discover()` function is the [`ManagerBLE#find()`](https://apps-sdk-v3.developer.homey.app/ManagerBLE.html#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:

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

{% endtab %}

{% tab title="Python" %}
An alternative to the `discover()` function is the [`ManagerBLE#find()`](https://python-apps-sdk-v3.developer.homey.app/manager/ble.html#homey.manager.ble.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:

```python
advertisement = await self.homey.ble.find("my_device_id")
```

{% endtab %}
{% endtabs %}

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.

### Subscribing to advertisements

If your device broadcasts its state in the advertisement itself — for example a sensor that exposes temperature in its service data — you can subscribe to it instead of polling. This delivers near-realtime updates without opening a GATT connection.

Advertisement subscriptions are only available on Homeys that support the `ble-advertisements` feature. Check support with `this.homey.hasFeature('ble-advertisements')` and provide a `find()`-based fallback for unsupported Homeys. See the [walkthrough below](#advertisements-and-connections) for a full example.

{% hint style="info" %}
The `ble-advertisements` feature is supported on the following Homey models:

* Homey Pro (Early 2023)
* Homey Pro Mini
* Homey Pro 2026
* Homey SHS
  {% endhint %}

## Connecting and Disconnecting devices

{% tabs %}
{% tab title="JavaScript" %}
Creating a connection to a device can be done by calling [`BleAdvertisement#connect()`](https://apps-sdk-v3.developer.homey.app/BleAdvertisement.html#connect).

```javascript
const peripheral = await advertisement.connect();
```

{% endtab %}

{% tab title="TypeScript" %}
Creating a connection to a device can be done by calling [`BleAdvertisement#connect()`](https://apps-sdk-v3.developer.homey.app/BleAdvertisement.html#connect).

```mts
const peripheral = await advertisement.connect();
```

{% endtab %}

{% tab title="Python" %}
Creating a connection to a device can be done by calling [`BleAdvertisement#connect()`](https://python-apps-sdk-v3.developer.homey.app/ble_advertisement.html#homey.ble_advertisement.BleAdvertisement.connect).

```python
peripheral = await advertisement.connect()
```

{% endtab %}
{% endtabs %}

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.

{% hint style="danger" %}
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.
{% endhint %}

{% tabs %}
{% tab title="JavaScript" %}
Disconnecting from a device is very trivial. If the device is already disconnected the call to [`BlePeripheral#disconnect()`](https://apps-sdk-v3.developer.homey.app/BlePeripheral.html#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.

```javascript
await peripheral.disconnect();
```

{% endtab %}

{% tab title="TypeScript" %}
Disconnecting from a device is very trivial. If the device is already disconnected the call to [`BlePeripheral#disconnect()`](https://apps-sdk-v3.developer.homey.app/BlePeripheral.html#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.

```mts
await peripheral.disconnect();
```

{% endtab %}

{% tab title="Python" %}
Disconnecting from a device is very trivial. If the device is already disconnected the call to [`BlePeripheral#disconnect()`](https://python-apps-sdk-v3.developer.homey.app/ble_peripheral.html#homey.ble_peripheral.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.

```python
await peripheral.disconnect()
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
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.
{% endhint %}

## 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

{% tabs %}
{% tab title="JavaScript" %}
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()`](https://apps-sdk-v3.developer.homey.app/BlePeripheral.html#read) and [`BlePeripheral#write()`](https://apps-sdk-v3.developer.homey.app/BlePeripheral.html#write). However, if more flexibility is desired it can also be achieved by accessing a characteristic immediately.
{% endtab %}

{% tab title="TypeScript" %}
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()`](https://apps-sdk-v3.developer.homey.app/BlePeripheral.html#read) and [`BlePeripheral#write()`](https://apps-sdk-v3.developer.homey.app/BlePeripheral.html#write). However, if more flexibility is desired it can also be achieved by accessing a characteristic immediately.
{% endtab %}

{% tab title="Python" %}
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()`](https://python-apps-sdk-v3.developer.homey.app/ble_peripheral.html#homey.ble_peripheral.BlePeripheral.read) and [`BlePeripheral#write()`](https://python-apps-sdk-v3.developer.homey.app/ble_peripheral.html#homey.ble_peripheral.BlePeripheral.write). However, if more flexibility is desired it can also be achieved by accessing a characteristic immediately.
{% endtab %}
{% endtabs %}

### 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.

{% tabs %}
{% tab title="JavaScript" %}

```javascript
// Reading
const data = await peripheral.read(serviceUuid, characteristicUuid);

// Writing
await peripheral.write(serviceUuid, characteristicUuid, data);
```

{% endtab %}

{% tab title="TypeScript" %}

```mts
// Reading
const data = await peripheral.read(serviceUuid, characteristicUuid);

// Writing
await peripheral.write(serviceUuid, characteristicUuid, data);
```

{% endtab %}

{% tab title="Python" %}

```python
# Reading
data = await peripheral.read(service_uuid, characteristic_uuid)

# Writing
await peripheral.write(service_uuid, characteristic_uuid, data)
```

{% endtab %}
{% endtabs %}

### 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.

{% tabs %}
{% tab title="JavaScript" %}

```javascript
// Reading
const data = await characteristic.read();

// Writing
await characteristic.write(data);
```

{% endtab %}

{% tab title="TypeScript" %}

```mts
// Reading
const data = await characteristic.read();

// Writing
await characteristic.write(data);
```

{% endtab %}

{% tab title="Python" %}

```python
# Reading
data = await characteristic.read()

# Writing
await characteristic.write(data)
```

{% endtab %}
{% endtabs %}

### Using a Handle

{% hint style="danger" %}
Reading and writing data using a **Handle** is not supported.
{% endhint %}

## Bluetooth Notifications

{% tabs %}
{% tab title="JavaScript" %}
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()`](https://apps-sdk-v3.developer.homey.app/BleCharacteristic.html#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.
{% endtab %}

{% tab title="TypeScript" %}
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()`](https://apps-sdk-v3.developer.homey.app/BleCharacteristic.html#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.
{% endtab %}

{% tab title="Python" %}
Starting with Homey 6.0 it is possible to use BLE notifications. In your app you can register for BLE notifications using the [`BleCharacteristic#subscribe_to_notifications()`](https://python-apps-sdk-v3.developer.homey.app/ble_characteristic.html#homey.ble_characteristic.BleCharacteristic.subscribe_to_notifications) 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.
{% endtab %}
{% endtabs %}

### Subscribing

{% tabs %}
{% tab title="JavaScript" %}

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

{% endtab %}

{% tab title="TypeScript" %}

```mts
await characteristic.subscribeToNotifications((data: Buffer) => {
  console.log("Received notification:", data);
});
```

{% endtab %}

{% tab title="Python" %}

```python
def on_notification(data: bytes):
    self.log("Received notification:", data)

await characteristic.subscribe_to_notifications(on_notification)
```

{% endtab %}
{% endtabs %}

### Unsubscribing

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

{% tabs %}
{% tab title="JavaScript" %}

```javascript
await characteristic.unsubscribeFromNotifications();
```

{% endtab %}

{% tab title="TypeScript" %}

```mts
await characteristic.unsubscribeFromNotifications();
```

{% endtab %}

{% tab title="Python" %}

```python
await characteristic.unsubscribe_from_notification()
```

{% endtab %}
{% endtabs %}

## Service Discovery

### Discover all services from a peripheral

{% tabs %}
{% tab title="JavaScript" %}

```javascript
const services = await peripheral.discoverServices();
```

{% endtab %}

{% tab title="TypeScript" %}

```mts
const services = await peripheral.discoverServices();
```

{% endtab %}

{% tab title="Python" %}

```python
services = await peripheral.discover_services()
```

{% endtab %}
{% endtabs %}

### Discover a single service

{% tabs %}
{% tab title="JavaScript" %}

```javascript
const service = await peripheral.getService(serviceUuid);
```

{% endtab %}

{% tab title="TypeScript" %}

```mts
const service = await peripheral.getService(serviceUuid);
```

{% endtab %}

{% tab title="Python" %}

```python
service = await peripheral.get_service(service_uuid)
```

{% endtab %}
{% endtabs %}

### Included Services

{% hint style="danger" %}
**Not (yet) supported**
{% endhint %}

## Characteristic Discovery

{% tabs %}
{% tab title="JavaScript" %}

```javascript
const characteristics = await service.discoverCharacteristics();
```

{% endtab %}

{% tab title="TypeScript" %}

```mts
const characteristics = await service.discoverCharacteristics();
```

{% endtab %}

{% tab title="Python" %}

```python
characteristics = await service.discover_characteristics()
```

{% endtab %}
{% endtabs %}

## 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](/guides/tools/bluetooth).

| 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, the App class can stay as it is. Instead, we must add a new driver.

{% hint style="info" %}
Make sure you have read about [Drivers and Devices](/the-basics/devices) before you continue
{% endhint %}

### 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:

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/driver/\<driver\_id>/driver.js" %}

```javascript
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;
```

{% endcode %}

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()`](https://apps-sdk-v3.developer.homey.app/ManagerBLE.html#find) method.
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/driver/\<driver\_id>/driver.mts" %}

```mts
import Homey from "homey";

export default class Driver extends Homey.Driver {
  async onPairListDevices(): Promise<object[]> {
    const advertisements = await this.homey.ble.discover();

    return advertisements
      .filter(advertisement => advertisement.localName === "my_device_name")
      .map(advertisement => ({
        name: advertisement.localName,
        data: {
          id: advertisement.uuid,
        },
        store: {
          peripheralUuid: advertisement.uuid,
        },
      }));
  }
}

```

{% endcode %}

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()`](https://apps-sdk-v3.developer.homey.app/ManagerBLE.html#find) method.
{% endtab %}

{% tab title="Python" %}
{% code title="/driver/\<driver\_id>/driver.py" %}

```python
from homey import driver
from homey.driver import ListDeviceProperties


class Driver(driver.Driver):
    async def on_pair_list_devices(self, view_data: dict) -> list[ListDeviceProperties]:
        advertisements = await self.homey.ble.discover()

        return [
            {
                "name": advertisement.local_name,
                "data": {"id": advertisement.uuid},
                "store": {"peripheralUuid": advertisement.uuid},
            }
            for advertisement in advertisements
            if advertisement.local_name == "my_device_name"
        ]


homey_export = Driver

```

{% endcode %}

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()`](https://python-apps-sdk-v3.developer.homey.app/manager/ble.html#homey.manager.ble.ManagerBLE.find) method.
{% endtab %}
{% endtabs %}

### 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](http://my.homey.app). In the debug logging for your app you can see that the driver and the device both have been initialized.

#### Advertisements and Connections

{% tabs %}
{% tab title="JavaScript" %}
Sensor-style devices that broadcast their state in their advertisement can subscribe to it, giving the device near-realtime updates without keeping a GATT connection open. Because advertisement subscriptions are only available on Homey models that report the `ble-advertisements` feature, the example below first checks `hasFeature` and falls back to periodic polling with `find()` when unsupported. The polling fallback stays correct, but for sensors that beacon every few seconds it is functionally slower than a live subscription.

For devices that also need to control state — writing data, toggling characteristics — see the *Writing and disconnecting* subsection below for the `connect()` pattern.

{% code title="/driver/\<driver\_id>/device.js" %}

```javascript
const Homey = require('homey');

const POLL_INTERVAL = 10 * 60 * 1000; // 10 minutes

class Device extends Homey.Device {
  async onInit() {
    if (this.homey.hasFeature('ble-advertisements')) {
      try {
        await this.homey.ble.subscribeToAdvertisements(
          this.getStore().peripheralUuid,
          { rateLimitMs: 5000 },
          advertisement => {
            this.onAdvertisement({
              address: advertisement.address,
              rssi: advertisement.rssi,
              manufacturerData: advertisement.manufacturerData,
              serviceData: advertisement.serviceData,
              localName: advertisement.localName,
              ts: Date.now(),
            });
          },
        );
        this.isSubscribed = true;
      } catch (err) {
        this.log('Could not subscribe to advertisements, falling back to polling:', err.message);
      }
    }

    if (!this.isSubscribed) {
      this.pollInterval = this.homey.setInterval(() => {
        this.poll().catch(this.error);
      }, POLL_INTERVAL);
    }
  }

  async poll() {
    const advertisement = await this.homey.ble.find(this.getStore().peripheralUuid);
    this.onAdvertisement({
      address: advertisement.address,
      rssi: advertisement.rssi,
      manufacturerData: advertisement.manufacturerData,
      serviceData: advertisement.serviceData,
      localName: advertisement.localName,
      ts: Date.now(),
    });
  }

  onAdvertisement(advertisement) {
    // parse and update capabilities
  }

  async onUninit() {
    if (this.isSubscribed) {
      await this.homey.ble.unsubscribeFromAdvertisements(this.getStore().peripheralUuid);
    }
    if (this.pollInterval) this.homey.clearInterval(this.pollInterval);
  }
}

module.exports = Device;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
Sensor-style devices that broadcast their state in their advertisement can subscribe to it, giving the device near-realtime updates without keeping a GATT connection open. Because advertisement subscriptions are only available on Homey models that report the `ble-advertisements` feature, the example below first checks `hasFeature` and falls back to periodic polling with `find()` when unsupported. The polling fallback stays correct, but for sensors that beacon every few seconds it is functionally slower than a live subscription.

For devices that also need to control state — writing data, toggling characteristics — see the *Writing and disconnecting* subsection below for the `connect()` pattern.

{% code title="/driver/\<driver\_id>/device.mts" %}

```mts
import Homey from "homey";

const POLL_INTERVAL = 10 * 60 * 1000; // 10 minutes

type AdvertisementData = {
  address: string;
  rssi: number;
  manufacturerData?: Buffer;
  serviceData?: Array<{ uuid: string; data: Buffer }>;
  localName?: string;
  ts: number;
};

export default class Device extends Homey.Device {
  isSubscribed = false;
  pollInterval?: NodeJS.Timeout;

  async onInit(): Promise<void> {
    if (this.homey.hasFeature("ble-advertisements")) {
      try {
        await this.homey.ble.subscribeToAdvertisements(
          this.getStore().peripheralUuid,
          { rateLimitMs: 5000 },
          advertisement => {
            this.onAdvertisement({
              address: advertisement.address,
              rssi: advertisement.rssi,
              manufacturerData: advertisement.manufacturerData,
              serviceData: advertisement.serviceData,
              localName: advertisement.localName,
              ts: Date.now(),
            });
          },
        );
        this.isSubscribed = true;
      } catch (err) {
        this.log("Could not subscribe to advertisements, falling back to polling:", (err as Error).message);
      }
    }

    if (!this.isSubscribed) {
      this.pollInterval = this.homey.setInterval(() => {
        this.poll().catch(this.error);
      }, POLL_INTERVAL);
    }
  }

  async poll(): Promise<void> {
    const advertisement = await this.homey.ble.find(this.getStore().peripheralUuid);
    this.onAdvertisement({
      address: advertisement.address,
      rssi: advertisement.rssi,
      manufacturerData: advertisement.manufacturerData,
      serviceData: advertisement.serviceData,
      localName: advertisement.localName,
      ts: Date.now(),
    });
  }

  onAdvertisement(advertisement: AdvertisementData): void {
    // parse and update capabilities
  }

  async onUninit(): Promise<void> {
    if (this.isSubscribed) {
      await this.homey.ble.unsubscribeFromAdvertisements(this.getStore().peripheralUuid);
    }
    if (this.pollInterval !== undefined) this.homey.clearInterval(this.pollInterval);
  }
}
```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
Sensor-style devices that broadcast their state in their advertisement can subscribe to it, giving the device near-realtime updates without keeping a GATT connection open. Because advertisement subscriptions are only available on Homey models that report the `ble-advertisements` feature, the example below first checks `has_feature` and falls back to periodic polling with `find()` when unsupported. The polling fallback stays correct, but for sensors that beacon every few seconds it is functionally slower than a live subscription.

For devices that also need to control state — writing data, toggling characteristics — see the *Writing and disconnecting* subsection below for the `connect()` pattern.

{% code title="/driver/\<driver\_id>/device.py" %}

```python
from homey import device
from homey.ble_advertisement import BleAdvertisement

POLL_INTERVAL = 10 * 60 * 1000


class Device(device.Device):
    is_subscribed: bool = False
    poll_interval: int | None = None

    async def on_init(self) -> None:
        if self.homey.has_feature("ble-advertisements"):
            try:
                await self.homey.ble.subscribe_to_advertisements(
                    self.get_store()["peripheralUuid"],
                    self.on_advertisement,
                    rate_limit_ms=5000,
                )
                self.is_subscribed = True
            except Exception as err:
                self.log("Could not subscribe to advertisements, falling back to polling:", err)

        if not self.is_subscribed:
            self.poll_interval = self.homey.set_interval(self.poll, POLL_INTERVAL)

    async def poll(self) -> None:
        advertisement = await self.homey.ble.find(self.get_store()["peripheralUuid"])
        self.on_advertisement(advertisement)

    def on_advertisement(self, advertisement: BleAdvertisement) -> None:
        # parse and update capabilities
        pass

    async def on_uninit(self) -> None:
        if self.is_subscribed:
            await self.homey.ble.unsubscribe_from_advertisements(
                self.get_store()["peripheralUuid"],
            )
        if self.poll_interval is not None:
            self.homey.clear_interval(self.poll_interval)


homey_export = Device
```

{% endcode %}
{% endtab %}
{% endtabs %}

#### Writing and disconnecting

{% tabs %}
{% tab title="JavaScript" %}
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:

```javascript
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`:

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

{% endtab %}

{% tab title="TypeScript" %}
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:

```mts
this.registerCapabilityListener("my_capability", async (): Promise<void> => {
  if (this.advertisement === undefined) return;
  const peripheral = await this.advertisement.connect();
  await peripheral.write("my_service", "my_characteristic", data).finally(() => {
    peripheral.disconnect();
  });
});
```

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

```mts
this.registerCapabilityListener("my_capability", async (): Promise<void> => {
  await this.peripheral?.write("my_service", "my_characteristic", data);
});
```

{% endtab %}

{% tab title="Python" %}
When you have your advertisement available in your app, for example as `self.advertisement`, a lot of things are possible. For example writing to a peripheral:

```python
async def my_capability_listener(value: device.CapabilityValue, **kwargs):
    if self.advertisement is None:
        return
    peripheral = await self.advertisement.connect()
    try:
        await peripheral.write("my_service", "my_characteristic", data)
    finally:
        await peripheral.disconnect()

self.register_capability_listener("my_capability", my_capability_listener)
```

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

```python
async def my_capability_listener(value: device.CapabilityValue, **kwargs):
    if self.peripheral is not None:
        await self.peripheral.write("my_service", "my_characteristic", data)

self.register_capability_listener("my_capability", my_capability_listener)
```

{% endtab %}
{% endtabs %}

#### Accessing a Service/Characteristic

A basic implementation for BLE notifications could be the following:

{% tabs %}
{% tab title="JavaScript" %}

```javascript
// 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];
```

{% endtab %}

{% tab title="TypeScript" %}

```mts
// Find the service
const services = await this.connection.discoverServices();
const dataService = services.find(service => service.uuid === "my_service_uuid");
if (dataService === undefined) {
  throw new Error("Could not find service");
}

// Find the characteristic
const dataCharacteristics = await dataService.discoverCharacteristics(["my_characteristic_uuid"]);
if (dataCharacteristics === undefined || dataCharacteristics.length === 0) {
  throw new Error("Could not find characteristic");
}
this.dataCharacteristic = dataCharacteristics[0];
```

{% endtab %}

{% tab title="Python" %}

```python
# Find the service
services = await self.connection.discover_services()
data_service = next((service for service in services if service.uuid == "my_service_uuid"), None)
if data_service is None:
    raise Exception("Could not find service")

# Find the characteristic
data_characteristics = await data_service.discover_characteristics(["my_characteristic_uuid"])
if not data_characteristics:
    raise Exception("Could not find characteristic")
self.data_characteristic = data_characteristics[0]
```

{% endtab %}
{% endtabs %}


# Z-Wave

Z-Wave is a two-way wireless communication standard on the 868 MHz - 915 MHz band. It is very similar to Zigbee.

Z-Wave is built around a principle called *Command Classes*. A Command Class is a group of *Commands*, that each can make a device perform an action, or request data.

Commands usually belong to one of 3 categories:

* "set" commands update a value
* "report" commands to let devices know the current value
* "get" commands are a request the current value, the device will send a report in response

As an example lets take a look at `COMMAND_CLASS_BASIC`, this Command Class is supported by most controllable devices. It has a `BASIC_SET` command, which sets a boolean (usually on/off), a `BASIC_GET`command, and a `BASIC_REPORT` command. Sending a `BASIC_GET` to the device will make the device send a `BASIC_REPORT` to Homey. The `BASIC_REPORT` contains the boolean (on/off) value.

Sending a command to a device from Homey is simple:

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require('homey');

class Device extends Homey.Device {
  async onInit() {
    const node = await this.homey.zwave.getNode(this);

    await node.CommandClass.COMMAND_CLASS_BASIC.BASIC_SET({ Value: true });
  }
}

module.exports = Device;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey, { ZwaveCommandClass } from "homey";

interface CommandClassBasic extends ZwaveCommandClass {
  BASIC_SET: (args: { Value: unknown }) => Promise<void>;
}

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    const node = await this.homey.zwave.getNode(this);

    await (node.CommandClass["COMMAND_CLASS_BASIC"] as CommandClassBasic).BASIC_SET({ Value: true });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/drivers/\<driver\_id>/device.py" %}

```python
from homey import device


class Device(device.Device):
    async def on_init(self) -> None:
        node = await self.homey.zwave.get_node(self)

        await node.command_classes["COMMAND_CLASS_BASIC"].send_command(
            "BASIC_SET", {"Value": True}
        )


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

{% hint style="info" %}
You can view working examples of Homey Apps that use Z-Wave at: [https://github.com/athombv/com.fibaro-example ](https://github.com/athombv/com.fibaro-example)and <https://github.com/athombv/com.danalock-example>
{% endhint %}

## Homey Pro Z-Wave User Manual

The Homey Pro Z-Wave User Manual applies to Homey Pro (Early 2023) and newer, Homey Pro mini, Homey Self-Hosted Server and Homey Cloud.

{% file src="/files/eJt19rrA0DTJJPlz6cgZ" %}

## Pairing

Tell Homey your driver supports a Z-Wave device by adding a `zwave` object to your driver manifest. To create a driver for a Z-Wave device you need to know the following properties of the device:

* `manufacturerId`
* `productTypeId`
* `productId`

To find out your device's IDs, pair it as a Basic Z-Wave Device. After successfully pairing you can find these values in the device settings. All Z-Wave devices are paired using the built-in Z-Wave pair wizard. Upon pairing a Z-Wave device, an App will be selected if all three IDs match.

You can customize the built-in Z-Wave pair wizard by supplying more specific instructions and a custom image in the `learnmode` property.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "class": "light",
  "capabilities": ["onoff"],
  "zwave": {
    "manufacturerId": 271,
    "productTypeId": [256],
    "productId": [260],
    "learnmode": {
      "image": "/drivers/<driver_id>/assets/learnmode.svg",
      "instruction": { "en": "Press the button on your device three times" }
    }
  }
}
```

{% endcode %}

{% hint style="info" %}
It is possible to add multiple `productTypeId` and `productId` values in order to support multiple devices with the same `Driver`.
{% endhint %}

Most Z-Wave devices are unpaired in the same way as they are paired but in case the unpair wizard benefits from custom instructions or a different image it is possible to provide `unlearnmode` options to your Z-Wave driver manifest. `unlearnmode` accepts the same properties as `learnmode`.

## Manifest

In addition to the essential driver `zwave` properties `manifacturerId`, `productTypeId` and `productId` there are properties allow you to configure the behaviour of your Z-Wave device.

{% hint style="warning" %}
Some of these properties configure the behaviour of your device while pairing, so changes to them will require you to remove and re-add your devices.
{% endhint %}

### Device settings

Z-Wave devices often have configuration parameters, in Homey these can be set in the device settings. The only thing you need to do is tell Homey which setting corresponds to which Z-Wave configuration parameter of the device. You can do this by adding a `zwave` property to the device setting that controls the configuration parameter. Read the [device setting guide](/the-basics/devices/settings) for more information.

{% code title="/drivers/\<driver\_id>/driver.settings.compose.json" %}

```javascript
[
  {
    "id": "minimum_brightness",
    "type": "number",
    "label": { "en": "Minimum brightness level" },
    "value": 1,
    "attr": { "min": 1,"max": 98 },
    "hint": { "en": "This parameter determines the minimal brightness." },
    "zwave": {
      "index": 13,
      "size": 1,
    }
  }
]
```

{% endcode %}

By default, the value is parsed as a **signed** integer. If your device requires the value to be interpreted as an **unsigned** integer, explicitly set the signed property to false in the setting's `zwave` configuration:

```javascript
  "zwave": {
    "index": 13,
    "size": 1,
    "signed": false,
  }
```

### Security

There are two types of Security in Z-Wave: Security 0 (legacy) and Security 2.

Most newer devices support Security 2 (S2). In S2 there are different keys available: Access, Authenticated, Unauthenticated (and S0). The actual encryption algorithm is the same, it just determines which devices know which keys.

Homey will always grant all S2 keys that a device requests.

{% hint style="info" %}
Homey Pro (2016-2019): Homey grants the highest requested key only.
{% endhint %}

#### Security 0 (S0)

Security 0 is not very secure and really inefficient, therefore it is not used by default. It is possible that devices only provide certain functionality when they are paired securely, for example a Z-Wave Doorlock can choose to only allow using `COMMAND_CLASS_LOCK` with secure communication. In this case your app should set the `requireSecure` property in your driver's `zwave` object to `true`. To find out what command classes require secure communication check your device's technical specifications.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "class": "lock",
  "capabilities": ["locked"],
  "zwave": {
    "manufacturerId": 270,
    "productTypeId": [8],
    "productId": [2],
    "requireSecure": true
  }
}
```

{% endcode %}

{% hint style="info" %}
By default Homey will use S2 if a device supports it. Due to the additional communication overhead of S0 your app needs to opt-into S0 by setting `requireSecure` to `true`.
{% endhint %}

### Default device configuration

After pairing a device, Homey can write configuration parameters to ensure they are set to the correct value using the `defaultConfiguration` parameter of the devices `zwave` options.

Configuration parameters can be 1, 2 or 4 bytes long, use the `size` property to tell Homey how many bytes to write. The `value` is a signed integer and its range is determined by the size of the parameter:

* -128, 127 for size=1
* -32768, -32767 for size=2
* -2147483648, 2147483647 for size=4

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "class": "light",
  "capabilities": ["onoff"],
  "zwave": {
    "manufacturerId": 271,
    "productTypeId": [256],
    "productId": [260],
    "defaultConfiguration": [
      {
        "id": 3,
        "size": 1,
        "value": 123
      }
    ]
  }
}
```

{% endcode %}

### Association Groups

Association groups allow Z-Wave devices to be linked together (associated) so that they can communicate directly without needing a central controller, like Homey. For some devices, Homey needs to be added to their association group to be able to receive status updates. For example devices may choose to only send "central scene activation" commands to their "lifeline" (the node in association group 1).

Check the device documentation from the manufacturer for an overview of the available association groups. Information can also be found here: <https://products.z-wavealliance.org/>

Homey is always added to association group 1, the Lifeline group. This way Homey can receive a notification when a device is factory reset for example. In that case the node is removed from Homey and the device is marked Unavailable.

To add Homey to other association groups automatically after pairing, add the corresponding group number to the `associationGroups` array property in the drivers `zwave` object.

Homey automatically determines whether to add a regular or multi-channel association to the group.

*Note: the* `associationGroupsMultiChannel` *is handled the same way as* `associationGroups` *so Homey is added to those groups as well using the correct association command class. It is used for backwards compatibility but it is recommended to use* `associationGroups` *and set the app compatibility to* `>=13.2.0`*.*

Associations between devices can be configured in the device settings. If a device supports `COMMAND_CLASS_ASSOCIATION_GRP_INFO` (most modern devices do), Homey uses this information to add a hint to the different groups explaining what they do. If this Command Class is not available or a more detailed hint is needed it can be provided in the `associationGroupsOptions` property.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "class": "light",
  "capabilities": ["onoff"],
  "zwave": {
    "manufacturerId": 271,
    "productTypeId": [256],
    "productId": [260],
    "associationGroups": [1, 3],
    "associationGroupsOptions": {
      "3": {
        "hint": { "en": "On/off signals from input 3" }
      }
    }
  }
}
```

{% endcode %}

#### Association Groups before Homey v13.2.0

In addition to "regular" (single-channel) associations devices with multiple endpoints can support "multi-channel association". In Homey versions before 13.2.0, Homey did not choose the regular or multi-channel association automatically. If multi-channel associations were necessary (to get updates from endpoints), they were configured using the `associationGroupsMultiChannel` property in the devices `zwave` options.

*Note: in Homey before 13.2.0 it was possible to opt-out of the Lifeline association (group 1) by providing an empty array. As of version 13.2.0 the Lifeline association is always added.*

### Battery Device Wake Up Interval

Z-Wave battery devices can have special behaviour to conserve power. These devices will send messages at any time but they are not always listening for messages. The interval with which it is possible to send messages to such a device is called the "wake up interval".

By default the manufacturer has chosen an apropriate wake up interval. It may be nessecary to override this default. In those cases you can set the `wakeUpInterval` property in the device's `zwave` options. This property controls the desired wake up interval in seconds. The range of allowed values by Homey is: 30-16777215 (30 seconds to 194 days). Make sure to use a value that the device supports.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "class": "light",
  "capabilities": ["onoff"],
  "zwave": {
    "manufacturerId": 271,
    "productTypeId": [256],
    "productId": [260],
    "wakeUpInterval": 900
  }
}
```

{% endcode %}

### Multi channel nodes

In Z-Wave a device can only implement a command class once. This means that technically a single Z-Wave device can only have a single switch, because it can only implement, for example, `COMMAND_CLASS_SWITCH_BINARY` once. In order to give devices the ability to implement a command class multiple times, Z-Wave has Multi Channel Nodes. A Multi Channel Node is a type of Z-Wave node that contains several "endpoints" which are each their own Z-Wave node. Each of these endpoints can individually implement, for example, `COMMAND_CLASS_SWITCH_BINARY`.

In Homey after pairing a Multi Channel node, several devices will be added to the devices overview if they are specified in the `multiChannelNodes` property in the `zwave` options.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "class": "light",
  "capabilities": ["onoff"],
  "zwave": {
    "manufacturerId": 271,
    "productTypeId": [256],
    "productId": [260],
    "multiChannelNodes": {
      "1": {
        "name": { "en": "MultiChannel device 1" },
        "class": "socket",
        "capabilities": ["onoff", "measure_power", "meter_power"],
        "icon": "/drivers/<driver_id>/assets/icon-multichannelnode1.svg"
      }
    }
  }
}
```

{% endcode %}

It is possible to override the settings of the Multi Channel Node by providing the `settings` option. This option takes the same values as the regular [Device Settings](/the-basics/devices#settings):

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```json
{
  "name": { "en": "My Driver" },
  "class": "light",
  "capabilities": ["onoff"],
  "zwave": {
    "manufacturerId": 271,
    "productTypeId": [256],
    "productId": [260],
    "multiChannelNodes": {
      "1": {
        "name": { "en": "MultiChannel device 1" },
        "class": "socket",
        "capabilities": ["onoff", "measure_power", "meter_power"],
        "icon": "/drivers/<driver_id>/assets/icon-multichannelnode1.svg",
        "settings": []
      }
    }
  }
}
```

{% endcode %}

## homey-zwavedriver

A library named [`homey-zwavedriver`](https://github.com/athombv/node-homey-zwavedriver) has been developed for Node.js to ease development of Z-Wave devices in Homey. It mainly maps Command Classes to Homey's capabilities. It is recommended for most app developers to use this library.

{% hint style="info" %}
[`homey-zwavedriver`](https://github.com/athombv/node-homey-zwavedriver) is only compatible with SDK version 3. Its predecessor [`homey-meshdriver`](https://github.com/athombv/node-homey-meshdriver) is available for SDK version 2.
{% endhint %}

```bash
npm install homey-zwavedriver
```

When you are using `homey-zwavedriver` your device should extend `ZwaveDevice` instead of `Homey.Device`. This class implements some useful helpers that you can use to integrate your Z-Wave device with Homey. When a new Device is initialized [`ZwaveDevice#onNodeInit()`](https://apps-sdk-v3.developer.homey.app/ZwaveDevice.html#onNodeInit) will be called. In this method you should register all the devices capabilities. `homey-zwavedriver` supplies a [`ZwaveDevice#registerCapability()`](https://apps-sdk-v3.developer.homey.app/ZwaveDevice.html#registerCapability) method that you only need to tell what capability your device has and which associated command class implements the capability.

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const { ZwaveDevice } = require('homey-zwavedriver');

class Device extends ZwaveDevice {
  async onNodeInit() {
    this.registerCapability('onoff', 'SWITCH_BINARY');
  }
}

module.exports = Device;
```

{% endcode %}

{% hint style="info" %}
If you are implementing a `light` device you can extend [`ZwaveLightDevice`](https://athombv.github.io/node-homey-zwavedriver/ZwaveLightDevice.html) instead. This class already implements all the expected behaviour for a Z-Wave light.
{% endhint %}

## Z-Wave API

{% hint style="warning" %}
If possible you should use [`homey-zwavedriver`](https://github.com/athombv/node-homey-zwavedriver) instead, only use the built-in Z-Wave API if you absolutely need to.
{% endhint %}

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require('homey');

class Device extends Homey.Device {
  async onInit() {
    // get the node by our Device's instance
    const node = await this.homey.zwave.getNode(this);

    // get the BASIC status
    node.CommandClass.COMMAND_CLASS_BASIC.BASIC_GET()
      .then((result) => {
        if (result["Value"]) {
          this.log("Device is turned on");
        } else {
          this.log("Device is turned off");
        }
      })
      .catch(this.error);

    // battery nodes can emit an 'online' event when they're available
    // you can send commands within 10s of this event, before the node goes to sleep again
    node.on("online", (online) => {
      if (online) {
        this.log("Device is online");
      } else {
        this.log("Device is offline");
      }
    });

    // register for 'report' events
    node.CommandClass.COMMAND_CLASS_BASIC.on("report", (command, report) => {
      this.log(command.name); // e.g. BASIC_REPORT
      this.log(report); // e.g. { Value: true }
    });
  }
}

module.exports = Device;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey, { ZwaveCommandClass } from "homey";

interface CommandClassBasic extends ZwaveCommandClass {
  BASIC_GET: () => Promise<{ Value: boolean }>;
}

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    // get the node by our Device's instance
    const node = await this.homey.zwave.getNode(this);

    // get the BASIC status
    await (node.CommandClass["COMMAND_CLASS_BASIC"] as CommandClassBasic)
      .BASIC_GET()
      .then(result => {
        if (result.Value) {
          this.log("Device is turned on");
        } else {
          this.log("Device is turned off");
        }
      })
      .catch(this.error);

    // battery nodes can emit an 'online' event when they're available
    // you can send commands within 10s of this event, before the node goes to sleep again
    node.on("online", (online: boolean) => {
      if (online) {
        this.log("Device is online");
      } else {
        this.log("Device is offline");
      }
    });

    // register for 'report' events
    node.CommandClass["COMMAND_CLASS_BASIC"].on("report", (command, report) => {
      this.log(command.name); // e.g. BASIC_REPORT
      this.log(report); // e.g. { Value: true }
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/drivers/\<driver\_id>/device.py" %}

```python
from homey import device


class Device(device.Device):
    async def on_init(self) -> None:
        # get the node by our Device's instance
        node = await self.homey.zwave.get_node(self)

        # get the BASIC status
        try:
            result = await node.command_classes["COMMAND_CLASS_BASIC"].send_command(
                "BASIC_GET"
            )
            if result.Value:
                self.log("Device is turned on")
            else:
                self.log("Device is turned off")
        except Exception as e:
            self.error(e)

        # battery nodes can emit an 'online' event when they're available
        # you can send commands within 10s of this event, before the node goes to sleep again
        def on_online(online: bool) -> None:
            if online:
                self.log("Device is online")
            else:
                self.log("Device is offline")

        node.on("online", on_online)

        # register for 'report' events
        def on_report(command, report) -> None:
            self.log(command.name)  # e.g. BASIC_REPORT
            self.log(report)  # e.g. { Value: true }

        node.command_classes["COMMAND_CLASS_BASIC"].on("report", on_report)


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

## Command Class Reference

* <https://z-wavealliance.org/development-resources-overview/z-wave-command-classes/>


# Z-Wave Firmware Updates

Allow Homey to update the firmware of Z-Wave devices using the Firmware Update Metadata Command Class.

Homey supports the Z-Wave Firmware Update Metadata Command Class. With this command class it is possible for Homey to update the firmware that is running on Z-Wave devices. Your app can provide firmware files, and Homey takes care of installing the firmware to the device.

{% hint style="warning" %}
Z-Wave firmware updates are supported since Homey firmware v13.2.0, and is available for Homey Pro (Early 2023, 2026, mini), Homey Self-Hosted Server, and Homey Cloud. You need at least Homey Mobile App v9.10.0 or higher in order to start updates.
{% endhint %}

### Getting Started <a href="#getting-started" id="getting-started"></a>

You can add firmware updates to your drivers. To do this, you'll need to gather the firmware files for your devices and create a `/drivers/<driver_id>/driver.firmware.compose.json` file. The `driver.firmware.compose.json` file contains metadata for Homey to be able to select the correct update for your devices.

For more information on the Z-Wave Firmware Update Metadata Command Class, refer to the [Z-Wave specification](https://z-wavealliance.org/development-resources-overview/specification-for-developers/).

{% hint style="info" %}
The `homey app driver firmware` Homey CLI command will take you through creating or updating the `driver.firmware.compose.json` file. (Requires `homey` v4.3.0 or higher.)
{% endhint %}

#### File Structure <a href="#file-structure" id="file-structure"></a>

The `driver.firmware.compose.json` contains the following fields:

* `updates`: A list of all firmware updates for the devices of this driver.
* `wakeInstruction`: A description of how a Sleepy End Device (e.g battery-powered devices) can be made active by the user to start the firmware update. This field is not required for non-sleepy devices (takes a [Translation Object](https://apps.developer.homey.app/~/changes/1149/the-basics/app/internationalization)).

**Updates**

Each item in `updates` describes a firmware update for specific Zigbee devices. Every update contains the following fields:

* `version`: The version of this firmware in `<major>.<minor>.<patch>` format.
* `changelog`: A brief description of the changes in this update (takes a [Translation Object](https://apps.developer.homey.app/~/changes/1149/the-basics/app/internationalization)).
* `device`: An object with the `manufacturerId`, `productTypeId` and `productId` combination that identifies the target device(s) for this update.
  * A driver can target multiple devices (see [Z-Wave Manifest](/wireless/z-wave#manifest)). This field allows making a firmware update only available for a specific subset of those devices.
  * Add a `hardwareVersion` field to make an update available for specific hardware versions of the device. When added, the field must match the device's reported hardware version through the Version Command Class.
* `files`: A list of firmware update files included in this update.
  * In most cases, you'll only have a single file for your update.
* `applicableTo`: A semver comparison string that determines whether the current version of the device can install this update. (E.g. `>1.2.3`).

#### Update Files

Each entry in `updates[].files` describes one firmware file. The following metadata fields are required for each file:

* `targetId`: The target chip of this file, if you have only a single file this is `0`.
* `size`: The file size in bytes.
* `name`: The file name. The file itself must be stored in `/drivers/<driver_id>/assets/firmware/<file name>`.
* `integrity`: A hash of the file in the format `<hash_name>:<hex_encoded_hash>`.
  * The following hash types are supported: `sha256`, `sha384`, `sha512`, `sha512-256`, `sha3-256`, `sha3-384`, `sha3-512`, `blake2b512`, `blake2s256`.

{% hint style="info" %}
The `homey app driver firmware` command will automatically fill in the `size`, `name`, and `integrity` fields for you. You'll have to provide the file's `targetId`.
{% endhint %}

Optionally you can add the following field to limit where a firmware update is available:

* `region`: The Z-Wave region where this file should be applied. If not set, it is applied globally.
  * Possible values are:
    * `ANZ`: Australia/New Zealand (919.8 MHz / 921.4 MHz)
    * `CN`: China (868.4 MHz)
    * `EU`: Europe (868.4 MHz / 869.85 MHz)
    * `HK`: Hong Kong (919.8 MHz)
    * `IL`: Israel (916 MHz)
    * `IN`: India (865.2 MHz)
    * `JP`: Japan (922.5 MHz / 923.9 MHz / 926.3 MHz)
    * `KR`: Korea (920.9 MHz / 921.7 MHz / 923.1 MHz)
    * `RU`: Russia (869 MHz)
    * `US`: United States of America (908.4 MHz / 916 MHz)

{% hint style="danger" %}
Not providing the region when this is required can cause Homey to install an update to a device in the wrong region, which can make the device unusable in the user's region.
{% endhint %}

The firmware file that is provided by the Homey app, should be the file that can be directly transferred to the Z-Wave device. Homey does not do any pre-processing of the file before sending it to the device. Homey cannot validate that it is a proper firmware file, because Z-Wave doesn't have a specification-defined file format for firmware files.

{% hint style="info" %}
When you upload your Homey App, the firmware files are stored separately from your App. Only when Homey starts installing an update for a device, it will download the required files.
{% endhint %}

### Example

Putting everything together, a `driver.firmware.compose.json` file with a single update will look like this:

{% code title="/driver/\<driver\_id>/driver.firmware.compose.json" %}

```json
{
  "wakeInstruction": {
    "en": "Hold the internal button for three seconds."
  },
  "updates": [
    {
      "version": "1.3.0",
      "changelog": {
        "en": "- Fixes issue X\n- Adds feature Y\n- Deprecates feature Z"
      },
      "device": {
        "manufacturerId": 1234,
        "productTypeId": [1, 2],
        "productId": [3, 4]
      },
      "files": [
        {
          "targetId": 0,
          "size": 262144,
          "name": "AwesomeSensor_v1.3.4.bin",
          "integrity": "sha256:dac89981aeb5352a8ddce9fbb5ab3ad5bff88d17e2f7937cc90947301ccfedd2"
        }
      ]
    }
  ]
}
```

{% endcode %}

#### Firmware Version <a href="#firmware-files" id="firmware-files"></a>

Homey uses the Version Command Class to determine the device's current firmware version.

* **If the device supports `VERSION_ZWAVE_SOFTWARE_GET`:** The firmware version is derived from the `Application Version` field of the `VERSION_ZWAVE_SOFTWARE_REPORT`. The version is formatted as `<byte_1>.<byte_2>.<byte_3>`.
* **If the device only supports `VERSION_GET`:** The firmware version is derived from the `Application Version` and `Application Sub Version` fields of the `VERSION_REPORT`. The patch field is always treated as `0`, resulting in the format `<application_version>.<application_sub_version>.0`.

#### Firmware Files <a href="#firmware-files" id="firmware-files"></a>

Homey Apps are responsible for delivering the correct firmware files to the appropriate devices. Before shipping a firmware update in your Homey App, ensure the following:

* **Device targeting:** Verify that each firmware file is mapped to the correct device(s). Delivering a firmware file to an incompatible device can cause irreversible damage or render it unresponsive.
* **End-to-end testing:** Test the full update flow on the intended physical hardware. Confirm that Homey can correctly initiate, transfer, and complete the firmware update process.
* **Validation before release:** Do not add a firmware update to your app until both of the above have been verified. Untested firmware updates should never reach end users.

## Update Selection

Homey will select the firmware for your device based on the following:

1. The `updates[].device` field must match the device's reported `manufacturerId`, `productTypeId`, and `productId`.
2. One of the files in an update should match the currently used Z-Wave region.
3. The device's current version should match the update's `applicableTo` field.

If multiple updates are found for the device, the update with the highest `version` field will be selected.

### Multiple Firmware Files

Some devices might require multiple firmware files for a single update (e.g. when it also needs to update the firmware of another chip in the device). If your device requires this, add multiple files to the update with different `targetId`s. Homey will automatically start sending a file for the next `targetId` after a previous one was completed.

### Update Failures <a href="#update-failures" id="update-failures"></a>

Firmware updates can fail. Homey considers an update failed in the following cases:

* **Explicit abort:** The device reports an error status during image transfer, actively aborting the update.
* **Stalled transfer:** The device stops requesting firmware chunks before the transfer is complete.
* **No reconnection:** The device fails to rejoin the Z-Wave network within the expected timeframe after a file has been fully transferred.

In all failure cases, the user will be notified via the firmware update screen and can retry the installation.


# Zigbee

Zigbee is a wireless communication standard that extends the IEEE 802.15.4  standard, it uses 2 different frequencies (2.4GHz, 869-915MHz), and is very  similar to Z-Wave.

Before you can get started developing Zigbee apps for Homey, a good place to start is with the basic Zigbee concepts and terminology. We will explain everything you need to know to get started here.

![](/files/-M_1OmcqIDGDH0LZx2Hv)

{% hint style="info" %}
You can view a working example of a Homey App that uses Zigbee at: <https://github.com/athombv/com.ikea.tradfri-example>
{% endhint %}

### Endpoints and Clusters

A Zigbee device is made up of one or more `endpoints`, these are collections of `clusters`. A cluster is essentially a capability of a Zigbee device.

A cluster can be implemented in two ways:

* As server
* As client

From the [Zigbee Cluster Specification](https://etc.athom.com/zigbee_cluster_specification.pdf): "Typically, the entity that stores the attributes of a cluster is referred to as the server of that cluster and an entity that affects or manipulates those attributes is referred to as the client of that cluster." More information on this can be found in the [Zigbee Cluster Specification](https://etc.athom.com/zigbee_cluster_specification.pdf) section 2.2.2.

### Commands and Attributes

Every cluster supports a set of commands and attributes. Attributes are properties of the cluster which can be read, written to and reported to any other bound node. An example would be the "current level" attribute implemented by the "level control" cluster, it represents the value of the current level of the device. The cluster may also support commands which are used to perform actions. An example would be the "move to level" command implemented by the "level control" cluster which makes the cluster change its current level to the level set by the command.

Commands can be sent in two directions:

1. from client to server
2. from server to client

The first is probably the most common direction, for example when sending a command from Homey (the client) to a bulb (the server) using the "onOff" cluster and the "toggle" command. The second is when a node (the server) sends a command to Homey (the client), for example a remote which sends the "toggle" command from the "onOff" cluster to Homey to indicate that the toggle button was pressed.

A cluster can report attributes, this means it will send a message to any bound node whenever the value of the attribute changes. Not all attributes are reportable, check the Zigbee Cluster Specification for the specifics of each attribute. In order to make a cluster report an attribute, a binding must be created between the reporting and receiving node.

### Bindings and Bound Clusters

The difference between server and client clusters is important for the following reason. Nodes can be receivers of commands (i.e. servers), or senders of commands (i.e. clients), and sometimes both. Receiving commands from a node most often requires a binding to be made from the controller (in this case Homey) to the cluster on the node, as well as an implementation of `BoundCluster` to receive and handle the incoming commands in your app. For more information on implementing a `BoundCluster` check out the API section.

### Groups

Zigbee allows another way of inter-node communication which is called groups. This concept can be compared to association groups in Z-Wave. It allows nodes to listen to broadcast messages from other nodes. For example, a remote control which broadcasts its commands to a set of light bulbs. The light bulbs would need to be in the same group as the remote is broadcasting on in order to be controlled with the remote. In order to group devices together the Find and Bind procedure can be used, often this means holding the controlling device close to the receiving device and initiating a commissioning process (how to initiate this process differs from device to device, check the device's manual for these instructions).

By default, Homey listens to group broadcast communication on its network, but the exact behavior depends on the platform:

* **Homey Pro (2016—2019) and Homey Bridge:** Homey listens to all group broadcasts on the network.
* **Homey Pro (2023 — 2026) and Homey Pro mini :** Homey only listens to group ID 0 and the groups advertised by Touchlink via `getGroups`.

### Example structure of a Zigbee Node

![](/files/-MYAUEiN9x0EaiNr39x4)

### Routers, End Devices and SEDs

There are a couple of different types of Zigbee devices:

* *Routers*: these are nodes on the network that are capable of routing messages between devices. Usually these are the non-battery powered devices. These extend the range of the Zigbee mesh network.
* *End Devices*: these are nodes on the network that are not capable of routing messages between devices. Usually these are battery powered devices. These do not extend the range of the Zigbee mesh network.

An important concept in Zigbee is Sleepy End Device (SED). SEDs are End Devices which are asleep most of the time, they only wake up to poll their parent (the Router they are paired to) for new messages every once in a while. A Router keeps track of a couple of messages targeted at the SED while it is asleep, so that the SED can retrieve these messages from the Router when it awakens.

When communicating with SEDs it is important to consider the possibility that it may not respond any time soon, or not at all. Most devices remain awake for a short amount of time directly after pairing, this is therefore the only time you can reliably communicate with the node. Additionally, since the Router has to store the messages for the SED, and the SED wakes up on a variable interval and fetches a single message every time, it is advised to only perform one request at a time for the most reliable communication.

A SED can be identified by the "Receive When Idle" flag in the nodes table in the [Zigbee developer tools](https://tools.developer.homey.app/tools/zigbee) or programmatically as a property of `ZigBeeNode`.

### Zigbee Cluster Specification

The full Zigbee Cluster Specification can be found at [Zigbee Cluster Specification (PDF)](https://etc.athom.com/zigbee_cluster_specification.pdf). This contains all information on clusters, commands and attributes you would need to create a driver for a Zigbee device.

## Pairing

The first step in creating a Zigbee driver is to retrieve the following properties of the device:

* `manufacturerName`
* `productId`

These can both be found by pairing the device as Basic Zigbee Device to Homey. Pairing is completely handled by Homey, similar to Z-Wave drivers and in contrast to other types of drivers, you don't have to implement your own pairing views. After pairing check the device settings or go to the [Zigbee developer tools](https://tools.developer.homey.app/tools/zigbee) and take a look at the nodes table where you can find the `manufacturerName` and `productId`.

The second step is finding out how the endpoints and clusters are structured on the device. In order to retrieve this information, interview the device from the Zigbee developer tools.

{% hint style="info" %}
For Sleepy End Devices this interview might take a while. After it finishes it will provide the required information.
{% endhint %}

## Manifest

After retrieving the `manufacturerName`, `productId` and endpoint definitions, the driver's manifest can be created. This is where is defined that this driver is for a Zigbee device, for which Zigbee device specifically (hence the `manufacturerName` and `productId`), and what the endpoints and clusters of this Zigbee device are (hence the endpoint definitions).

This information is added to the `zigbee` object to the driver's manifest.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "class": "socket",
  "capabilities": ["onoff", "dim"],
  "zigbee": {
    "manufacturerName": "DummyManuf",
    "productId": ["control outlet 123"],
    "endpoints": {
      "1": {
        "clusters": [0, 4, 5, 6],
        "bindings": [6]
      }
    },
    "learnmode": {
      "image": "/drivers/my_driver/assets/learnmode.svg",
      "instruction": { "en": "Press the button on your device three times" }
    }
  }
}
```

{% endcode %}

The `zigbee` object contains the following properties:

* `manufacturerName`: This is the manufacturer id which is needed to identify the device.
* `productId`: This is the product id which is needed to identify the device. It is possible to add multiple product ids if the driver targets a couple of very similar devices with different product ids.
* `endpoints`: This is the endpoint definition for the device. Only the endpoints and clusters listed here will be available on the `ZCLNode` instance (see the documentation on `homey-zigbeedriver` and `zigbee-clusters` in below in the API section). The keys of the `endpoints` object refer to the endpoint id of the node.
  * `clusters`: This lists the cluster ids we want to implement as client. This means, the clusters we want to send commands to, or read attributes from, on the remote node.
  * `bindings`: This lists the cluster ids we want to implement as server. This means, the clusters we want to be able to receive commands on from a remote node. For each entry in `bindings` a bind request will be made to the remote node during pairing. In case you want to implement attribute reporting for a specific cluster, add the cluster id here and the required binding will be made during pairing.

## Dependencies

In order to create a Zigbee driver a Homey App needs the following dependencies:

* [homey-zigbeedriver](https://athombv.github.io/node-homey-zigbeedriver/)
* [zigbee-clusters](https://github.com/athombv/node-zigbee-clusters)

You can install these dependencies by running:

```bash
npm install --save homey-zigbeedriver zigbee-clusters
```

{% hint style="warning" %}
Note that zigbee-clusters is a `peerDependency` of homey-zigbeedriver. This means that homey-zigbeedriver depends on zigbee-clusters being installed with a compatible version.
{% endhint %}

## Driver and Device

Finally, the driver needs to be created. Most of the time we can suffice with only `/drivers/my_driver/device.js`. Please refer to the Drivers guide for more information on this topic.

The easiest way to implement a Zigbee driver is to use `homey-zigbeedriver`, which is a library we've created which does a lot of the heavy lifting for Zigbee apps. Take a look at the API section for the specifics of this library. Below, a number of basic implementations of various aspects of a Zigbee driver is demonstrated based on `homey-zigbeedriver`.

### Best Practices for Device Initialization

Avoid initiating communication with the node in the `onInit` or `onNodeInit` phases. Zigbee may not be ready during the initialization of the device, leading to undesirable consequences. Communicating with the node in `onInit` or `onNodeInit` can result in queuing numerous requests when the app is started, potentially causing performance bottlenecks.

If communication with the node is attempted in `onInit` or `onNodeInit` without catching the request promise, the device may become unavailable, accompanied by an error message indicating that Zigbee was not ready. This is true for versions up to `homey-zigbeedriver@2.1.3`. While versions 2.1.4 and newer prevent the device from becoming unavailable, there is a risk that the `onInit` or `onNodeInit` may not complete as expected.

If there's a genuine need to request data in the `onInit` or `onNodeInit` phases, it's important to handle the request promise appropriately. Failing to catch the promise may lead to unforeseen issues. Below is a recommended approach:

#### Do

```javascript
const { ZigBeeDevice } = require("homey-zigbeedriver");

class Device extends ZigBeeDevice {
  async onNodeInit({ zclNode }) {
    // Read the "onOff" attribute from the "onOff" cluster
    const currentOnOffValue = await zclNode.endpoints[1].clusters.onOff.readAttributes(
      ["onOff"]
    ).catch(err => { this.error(err); /* Always catch Promises, especially in onNodeInit */ });
  }
}

module.exports = Device;
```

#### Don't

```javascript
const { ZigBeeDevice } = require("homey-zigbeedriver");

class Device extends ZigBeeDevice {
  async onNodeInit({ zclNode }) {
    // Read the "onOff" attribute from the "onOff" cluster
    const currentOnOffValue = await zclNode.endpoints[1].clusters.onOff.readAttributes(
      ["onOff"]
    );
  }
}

module.exports = Device;
```

#### First initialization

In some cases, you might want to perform certain actions only the first time `onInit` or `onNodeInit` is called, right after adding the device to Homey. You can achieve this using `ZigbeeDevice.isFirstInit()` from `homey-zigbeedriver`. Here's an example:

```javascript
const { ZigBeeDevice } = require("homey-zigbeedriver");

class Device extends ZigBeeDevice {
  async onNodeInit({ zclNode }) {
  
    // Only if this is the first time this device is initialized, right after
    // adding the device to Homey
    if (this.isFirstInit() === true) {
      // Read the "onOff" attribute from the "onOff" cluster
      const currentOnOffValue = await zclNode.endpoints[1].clusters.onOff.readAttributes(
        ["onOff"]
      ).catch(err => { this.error(err); /* Always catch Promises, especially in onNodeInit */ });
    }
  }
}

module.exports = Device;
```

### Debugging

When developing a Zigbee driver, it can be very useful to see all Zigbee communication between Homey and the Zigbee node. Debug logging for this can be enabled very easily as follows.

> It is not recommended to keep this debug logging enabled when publishing your app.

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const { ZigBeeDevice } = require("homey-zigbeedriver");
const { debug } = require("zigbee-clusters");

// Enable debug logging of all relevant Zigbee communication
debug(true);

class Device extends ZigBeeDevice {}

module.exports = Device;
```

{% endcode %}

Example logging of a received attribute report frame:

```
2020-08-07T13:04:30.933Z zigbee-clusters:cluster ep: 1, cl: illuminanceMeasurement (1024) received frame reportAttributes illuminanceMeasurement.reportAttributes {
  attributes: <Buffer 00 00 21 7e 00>
}
```

### Commands

The `zclNode` is an instance of `ZCLNode` as exported by `zigbee-clusters` (check the API section for more information on this library). It can be used to directly communicate with the node using the Zigbee Cluster Library (ZCL).

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const { ZigBeeDevice } = require("homey-zigbeedriver");

class Device extends ZigBeeDevice {
  async onNodeInit({ zclNode }) {
    // Send the "toggle" command to cluster "onOff" on endpoint 1
    await zclNode.endpoints[1].clusters.onOff.toggle()
      .catch(err => { this.error(err); /* Always catch Promises, especially in onNodeInit */ });

    // Read the "onOff" attribute from the "onOff" cluster
    const currentOnOffValue = await zclNode.endpoints[1].clusters.onOff.readAttributes(
      ["onOff"]
    ).catch(err => { this.error(err); /* Always catch Promises, especially in onNodeInit */ });
  }
}

module.exports = Device;
```

{% endcode %}

### Capabilities

Using `homey-zigbeedriver` it is very easy to map Homey's capabilities to Zigbee clusters. The library contains a set of [system capabilities](https://github.com/athombv/node-homey-zigbeedriver/tree/master/lib/system/capabilities), which are basically very common mappings between capabilities and clusters. It is possible to extend these system capabilities when registering them, for more information take a look at the `homey-zigbeedriver` [documentation](https://github.com/athombv/node-homey-zigbeedriver) and API section.

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const { ZigBeeDevice } = require("homey-zigbeedriver");
const { CLUSTER } = require("zigbee-clusters");

class Device extends ZigBeeDevice {
  async onNodeInit({ zclNode }) {
    // This maps the `onoff` capability to the "onOff" cluster
    this.registerCapability("onoff", CLUSTER.ON_OFF);

    // This maps the `dim` capability to the "levelControl" cluster
    this.registerCapability("dim", CLUSTER.LEVEL_CONTROL);
  }
}

module.exports = Device;
```

{% endcode %}

### Attribute Reporting

As described above, nodes can report attribute changes to any other bound node. This often requires a binding to be made to the node that should report. This can be done using the driver's manifest `bindings` property. After that, the attribute reporting must be configured on the node's cluster. The example below demonstrates two ways of configuring attribute reporting:

1. Directly configuring the attribute reporting.
2. Configuring the attribute reporting in combination with mapping it to a capability.

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const { ZigBeeDevice } = require("homey-zigbeedriver");
const { CLUSTER } = require("zigbee-clusters");

class Device extends ZigBeeDevice {
  async onNodeInit({ zclNode }) {
    // 1.1.) Configure attribute reporting without registering a capability
    await this.configureAttributeReporting([
      {
        endpointId: 1,
        cluster: CLUSTER.COLOR_CONTROL,
        attributeName: "currentHue",
        minInterval: 0,
        maxInterval: 300,
        minChange: 10,
      },
    ]).catch(err => { this.error(err); /* Always catch Promises, especially in onNodeInit */ });

    // 1.2.) Listen to attribute reports for the above configured attribute reporting
    zclNode.endpoints[1].clusters.colorControl.on(
      "attr.currentHue",
      (currentHue) => {
        // Do something with the received attribute report
      }
    );

    // 2) This maps the `dim` capability to the "levelControl" cluster and additionally configures attribute reporting for the `currentLevel` attribute as specified in the system capability
    this.registerCapability("dim", CLUSTER.LEVEL_CONTROL, {
      reportOpts: {
        configureAttributeReporting: {
          minInterval: 0, // No minimum reporting interval
          maxInterval: 60000, // Maximally every ~16 hours
          minChange: 5, // Report when value changed by 5
        },
      },
    });
  }
}

module.exports = Device;
```

{% endcode %}

For more information on configuring attribute reporting check [ZigBeeDevice#configureAttributeReporting](https://athombv.github.io/node-homey-zigbeedriver/ZigBeeDevice.html#configureAttributeReporting).

> It is recommended to configure attribute reporting for sleepy end devices. This ensures that the device periodically sends a message to Homey, allowing Homey to verify that the device is still present on the network. In the future, Homey may use this information to show users which devices have been unresponsive for some time.

### Bindings and Groups

In order to act on incoming commands from bindings or groups it is necessary to implement a `BoundCluster` (this is exported by `zigbee-clusters`):

{% code title="/lib/LevelControlBoundCluster.js" %}

```javascript
const { BoundCluster } = require("zigbee-clusters");

class LevelControlBoundCluster extends BoundCluster {
  constructor({ onMove }) {
    super();
    this._onMove = onMove;
  }

  // This function name is directly derived from the `move`
  // command in `zigbee-clusters/lib/clusters/levelControl.js`
  // the payload received is the payload specified in
  // `LevelControlCluster.COMMANDS.move.args`
  move(payload) {
    this._onMove(payload);
  }
}

module.exports = LevelControlBoundCluster;
```

{% endcode %}

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const LevelControlBoundCluster = require("../../lib/LevelControlBoundCluster");

const { ZigBeeDevice } = require("homey-zigbeedriver");
const { CLUSTER } = require("zigbee-clusters");

class Device extends ZigBeeDevice {
  async onNodeInit({ zclNode }) {
    // Register the `BoundCluster` implementation with the `ZCLNode`
    zclNode.endpoints[1].bind(
      CLUSTER.LEVEL_CONTROL.NAME,
      new LevelControlBoundCluster({
        onMove: (payload) => {
          // Do something with the received payload
        },
      })
    );
  }
}

module.exports = Device;
```

{% endcode %}

For more information on implementing a bound cluster checkout the `zigbee-clusters` documentation on [Implementing a bound cluster](https://athombv.github.io/node-zigbee-clusters/).

### Custom Clusters

It is possible to implement custom clusters, these are often manufacturer specific implementations of existing clusters. This is too in-depth to cover here, but is documented in [Implementing a cluster](https://github.com/athombv/node-zigbee-clusters#implementing-a-cluster) and [Implementing a custom cluster](https://github.com/athombv/node-zigbee-clusters#implementing-a-custom-cluster).

## Sub Devices

> This feature is available as of Homey v5.0.0 and requires homey-zigbeedriver\@1.6.0 or higher. Additionally, `Driver` must extend `ZigBeeDriver` as exported by homey-zigbeedriver.

In some cases a single physical Zigbee device should be represented as multiple devices in Homey after pairing. Most physical Zigbee devices should be represented by a single Homey device for the best user experience. However, for some devices, like a socket with multiple outputs, the user experience is improved when there are multiple Homey devices representing it. Sub devices enable you to automatically create multiple devices in Homey after pairing a single physical Zigbee device. In order for Homey to create multiple instances of `Device` you need to define a `devices` property in your Zigbee driver's manifest. The keys of this object represent a unique sub device ID which will be added to the device data object as `subDeviceId`. For example, based on the manifest below:

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const { ZigBeeDevice } = require("homey-zigbeedriver");

class Device extends ZigBeeDevice {
  async onNodeInit({ zclNode }) {
    const { subDeviceId } = this.getData();
    // subDeviceId === 'secondOutlet'
  }
}

module.exports = Device;
```

{% endcode %}

This can be used in `device.js` to discern between the root device, and the various sub devices.

The sub device object can contain any property the root device can contain, e.g. `class`, `name`, and `capabilties`. Properties that are omitted in the sub device will be copied over from the root device. If the sub device should not get the same settings as the root device, make sure to add the `settings: []` property to the sub device, as demonstrated below.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "class": "socket",
  "capabilities": ["onoff", "dim"],
  "zigbee": {
    "manufacturerName": "DummyManuf",
    "productId": ["control outlet 123"],
    "endpoints": {
      "1": {
        "clusters": [0, 4, 5, 6],
        "bindings": [6]
      }
    },
    "devices": {
      "secondOutlet": {
        "class": "light",
        "capabilities": ["onoff"],
        "name": { "en": "Second Outlet" },
        "settings": []
      }
    }
  }
}
```

{% endcode %}

Each `Device` will have access to the same `ZCLNode` instance and can access all endpoints. By default, for each sub device a device instance, as exported in `device.js`, will be created. If the implementation of `device.js` is quite different between sub devices you can use [`Driver#onMapDeviceClass()`](https://apps-sdk-v3.developer.homey.app/Driver.html#onMapDeviceClass) to properly separate the logic for the root and sub devices by implementing multiple `Device` classes.

{% code title="/drivers/\<driver\_id>/driver.js" %}

```javascript
const { ZigBeeDriver } = require("homey-zigbeedriver");

const RootDevice = require("./device.js");
const SecondOutletDevice = require("./secondOutlet.device.js");

class Driver extends ZigBeeDriver {
  onMapDeviceClass(device) {
    if (device.getData().subDeviceId === "secondOutlet") {
      return SecondOutletDevice;
    } else {
      return RootDevice;
    }
  }
}

module.exports = Driver;
```

{% endcode %}

## ZCL Intruder Alarm Systems (IAS)

As of Homey v13.1.2, Homey automatically handles full IAS Zone enrollment. Just add cluster id `1280` (IAS Zone) to the `clusters` array in driver.compose.json for the relevant endpoint. `onZoneEnrollRequest` events are no longer forwarded to apps. Homey always assigns `zoneId: 0`.

## API

There are three Zigbee API levels which all build on top of each other with increasing complexity to interact with Zigbee devices on Homey:

![](/files/-MYAUEiUKUHWMYRjP2no)

In general it is likely you will only need [homey-zigbeedriver](https://github.com/athombv/node-homey-zigbeedriver), and possibly [zigbee-clusters](https://github.com/athombv/node-zigbee-clusters) if you want to do some more advanced things. The Zigbee API is what `zigbee-clusters` is built on. It is not advised to use this API directly, but it is there in case you need it.

### 1. homey-zigbeedriver

This library for Node.js is developed by Athom to make it easier to create a driver for Zigbee devices.

It exposes classes which can be extended in your app and other useful functionality. The most important class is [`ZigBeeDevice`](https://athombv.github.io/node-homey-zigbeedriver/ZigBeeDevice.html). This class handles getting a `ZigBeeNode` and uses [`zigbee-clusters`](https://github.com/athombv/node-zigbee-clusters) to extend the ZigBeeNode with Zigbee Cluster Library (ZCL) functionality. Doing this enables a developer to directly communicate with the ZigBeeNode using ZCL very easily. The basic usage is demonstrated below, for more in-depth information take a look at the `homey-zigbeedriver` [documentation](https://athombv.github.io/node-homey-zigbeedriver/) or the [source code on GitHub](https://github.com/athombv/node-homey-zigbeedriver).

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const { ZigBeeDevice } = require("homey-zigbeedriver");

class Device extends ZigBeeDevice {
  async onNodeInit({ zclNode }) {
    await zclNode.endpoints[1].clusters.onOff.toggle()
      .catch(err => { this.error(err); /* Always catch Promises, especially in onNodeInit */ });
  }
}

module.exports = Device;
```

{% endcode %}

Note: for Zigbee light devices (e.g. bulbs and spots) we created [ZigBeeLightDevice](https://athombv.github.io/node-homey-zigbeedriver/ZigBeeLightDevice.html), this class can be extended in your driver and will by default handle all light related functionality.

### 2. zigbee-clusters

This is the library used by [homey-zigbeedriver](https://github.com/athombv/node-homey-zigbeedriver) to expose the Zigbee Cluster Library (ZCL) functionality. It is currently not available for Python apps. It implements all the clusters that are accessible through `zclNode` in [`lib/clusters`](https://github.com/athombv/node-zigbee-clusters/tree/master/lib/clusters). It is very easy to add new clusters, or add attributes or commands to an existing cluster, merely by changing the definition in one of the cluster files (e.g. the [onOff cluster](https://github.com/athombv/node-zigbee-clusters/blob/master/lib/clusters/onOff.js)). If you are interested in how this library works, take a look at the [Zigbee Cluster Specification (PDF)](https://etc.athom.com/zigbee_cluster_specification.pdf). The basic usage of this library is demonstrated below, note that it is usually better to use [`ZigBeeDevice`](https://athombv.github.io/node-homey-zigbeedriver/ZigBeeDevice.html) exported by [homey-zigbeedriver](https://github.com/athombv/node-homey-zigbeedriver) instead.

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require("homey");
const { ZCLNode, CLUSTER } = require("zigbee-clusters");

class Device extends Homey.Device {
  async onInit() {
    // Get ZigBeeNode instance from ManagerZigBee
    const node = await this.homey.zigbee.getNode(this);

    // Create ZCLNode instance
    const zclNode = new ZCLNode(node);

    // Interact with the node
    await zclNode.endpoints[1].clusters.onOff.toggle()
      .catch(err => { this.error(err); /* Always catch Promises */ });
  }
}

module.exports = Device;
```

{% endcode %}

There are a few cases where you *need* to interact with [zigbee-clusters](https://github.com/athombv/node-zigbee-clusters) in your app.

#### 1. Interacting with homey-zigbeedriver

In order to inform `homey-zigbeedriver` about which cluster we are targeting we need to import the cluster specification from `zigbee-clusters` as demonstrated below.

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const { CLUSTER } = require("zigbee-clusters");
const { ZigBeeDevice } = require("homey-zigbeedriver");

class Device extends ZigBeeDevice {
  async onNodeInit({ zclNode }) {
    // Register onoff capability
    this.registerCapability("onoff", CLUSTER.ON_OFF);
  }
}

module.exports = Device;
```

{% endcode %}

This ensures the cluster to be registered will be available in `zigbee-clusters`.

#### 2. Implementing a new cluster, or improving an existing cluster

To add or improve clusters for `zigbee-clusters` , changes can be made to the cluster definition in [`lib/clusters`](https://github.com/athombv/node-zigbee-clusters/tree/master/lib/clusters). For more information on how to do this, check out [Implementing a cluster](https://github.com/athombv/node-zigbee-clusters#implementing-a-cluster).

#### 3. Implementing a bound cluster

As mentioned in the introduction, in order to receive commands from a node, a binding must be made to the respective cluster. This can be done by listing the cluster id in the `bindings` array in the driver's manifest. Please refer to [Implementing a bound cluster](https://github.com/athombv/node-zigbee-clusters#implementing-a-bound-cluster) for more information on how to create a `BoundCluster` in your driver.

#### 4. Implementing a custom cluster

Zigbee device manufacturers are allowed to implement custom clusters for their devices. In order to use such a custom cluster in your driver you need to extend an existing cluster with the custom behaviour. Check out [Implementing a custom cluster](https://github.com/athombv/node-zigbee-clusters#implementing-a-custom-cluster) on how to do this exactly.

### 3. Zigbee API

The Zigbee API can be used for directly communicating with a `ZigBeeNode` if needed. In general it is advised not to use this API and take a look at [zigbee-clusters](https://github.com/athombv/node-zigbee-clusters) and [homey-zigbeedriver](https://github.com/athombv/node-homey-zigbeedriver) which are libraries built on top of the Zigbee API which do most of the heavy lifting and make it even easier to develop Zigbee apps for Homey.

In the unexpected case that you do want to access the Zigbee API take a look below for a basic example.

{% tabs %}
{% tab title="JavaScript" %}
First, the `ZigBeeNode` must be retrieved from [`ManagerZigBee`](https://apps-sdk-v3.developer.homey.app/ManagerZigBee.html).

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require("homey");

class Device extends Homey.Device {
  async onInit() {
    const node = await this.homey.zigbee.getNode(this);
  }
}

module.exports = Device;
```

{% endcode %}

Next, we can use the Zigbee API to directly communicate with the `ZigBeeNode` using [`ZigBeeNode#sendFrame()`](https://apps-sdk-v3.developer.homey.app/ZigBeeNode.html#sendFrame) and [`ZigBeeNode#handleFrame()`](https://apps-sdk-v3.developer.homey.app/ZigBeeNode.html#handleFrame)

{% hint style="warning" %}
Important: override the `handleFrame` method on `ZigBeeNode`, this method is called when a frame is received and if it is not overridden it will throw.
{% endhint %}

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require("homey");

class Device extends Homey.Device {
  async onInit() {
    const node = await this.homey.zigbee.getNode(this);
    node.handleFrame = (endpointId, clusterId, frame, meta) => {
      if (endpointId === 1 && clusterId === 6) {
        // The node sent a frame to Homey from endpoint 1 and cluster 'onOff'
      }
    };

    // Send a frame to endpoint 1, cluster 6 ('onOff') which turns the node on
    await node.sendFrame(
      1, // endpoint id
      6, // cluster id
      Buffer.from([
        1, // frame control
        0, // transaction sequence number
        1, // command id ('on')
      ])
    ).catch(err => { this.error(err); /* Always catch Promises */ });

    // Send a frame to endpoint 1, cluster 6 ('onOff') which turns the node off
    await node.sendFrame(
      1, // endpoint id
      6, // cluster id
      Buffer.from([
        1, // frame control
        1, // transaction sequence number
        0, // command id ('off')
      ])
    ).catch(err => { this.error(err); /* Always catch Promises */ });
  }
}

module.exports = Device;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
First, the `ZigBeeNode` must be retrieved from [`ManagerZigBee`](https://apps-sdk-v3.developer.homey.app/ManagerZigBee.html).

{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey from "homey";

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    const node = await this.homey.zigbee.getNode(this);
  }
}

```

{% endcode %}

Next, we can use the Zigbee API to directly communicate with the `ZigBeeNode` using [`ZigBeeNode#sendFrame()`](https://apps-sdk-v3.developer.homey.app/ZigBeeNode.html#sendFrame) and [`ZigBeeNode#handleFrame()`](https://apps-sdk-v3.developer.homey.app/ZigBeeNode.html#handleFrame)

{% hint style="warning" %}
Important: override the `handleFrame` method on `ZigBeeNode`, this method is called when a frame is received and if it is not overridden it will throw.
{% endhint %}

{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey from "homey";

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    const node = await this.homey.zigbee.getNode(this);
    node.handleFrame = async (endpointId: number, clusterId: number, frame: Buffer, meta: object): Promise<void> => {
      if (endpointId === 1 && clusterId === 6) {
        // The node sent a frame to Homey from endpoint 1 and cluster 'onOff'
      }
    };

    await node
      .sendFrame(
        1, // endpoint id
        6, // cluster id
        Buffer.from([
          1, // frame control
          0, // transaction sequence number
          1, // command id ('on')
        ]),
      )
      .catch(this.error /* Always catch Promises */);

    // Send a frame to endpoint 1, cluster 6 ('onOff') which turns the node off
    await node
      .sendFrame(
        1, // endpoint id
        6, // cluster id
        Buffer.from([
          1, // frame control
          1, // transaction sequence number
          0, // command id ('off')
        ]),
      )
      .catch(this.error /* Always catch Promises */);
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
First, the `ZigBeeNode` must be retrieved from [`ManagerZigBee`](https://python-apps-sdk-v3.developer.homey.app/manager/zigbee.html).

{% code title="/drivers/\<driver\_id>/device.py" %}

```python
from homey import device


class Device(device.Device):
    async def on_init(self) -> None:
        node = await self.homey.zigbee.get_node(self)


homey_export = Device

```

{% endcode %}

Next, we can use the Zigbee API to directly communicate with the `ZigBeeNode` using [`ZigBeeNode#send_frame()`](https://python-apps-sdk-v3.developer.homey.app/zigbee_node.html#homey.zigbee_node.ZigbeeNode.send_frame) and [`ZigBeeNode#handle_frame()`](https://python-apps-sdk-v3.developer.homey.app/zigbee_node.html#homey.zigbee_node.ZigbeeNode.handle_frame)

{% hint style="warning" %}
Important: override the `handle_frame` method on `ZigBeeNode`, this method is called when a frame is received and if it is not overridden it will throw.
{% endhint %}

{% code title="/drivers/\<driver\_id>/device.py" %}

```python
from homey import device


class Device(device.Device):
    async def on_init(self) -> None:
        node = await self.homey.zigbee.get_node(self)

        async def handle_frame(
            endpoint_id: int, cluster_id: int, frame: bytes, meta: dict
        ) -> None:
            if endpoint_id == 1 and cluster_id == 6:
                # The node sent a frame to Homey from endpoint 1 and cluster 'onOff'
                ...

        node.handle_frame = handle_frame

        try:
            await node.send_frame(
                1,  # endpoint id
                6,  # cluster id
                bytes(
                    [
                        1,  # frame control
                        0,  # transaction sequence number
                        1,  # command id ('on')
                    ]
                ),
            )
        except Exception as e:
            self.error(e)  # Always handle exceptions

        try:
            await node.send_frame(
                1,  # endpoint id
                6,  # cluster id
                bytes(
                    [
                        1,  # frame control
                        1,  # transaction sequence number
                        0,  # command id ('off')
                    ]
                ),
            )
        except Exception as e:
            self.error(e)  # Always handle exceptions


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}


# Zigbee Firmware Updates

Allow Homey to update the firmware of Zigbee devices using the Zigbee Over-the-Air Upgrade Cluster.

Homey supports the Zigbee Over-the-Air Upgrade cluster. With this cluster it is possible for Homey to update the firmware that is running on Zigbee devices. Your app can provide firmware files, and Homey takes care of installing the firmware to the device.

{% hint style="warning" %}
Zigbee firmware updates are supported since Homey firmware v13.2.0, and is available for Homey Pro (Early 2023, 2026, mini), Homey Self-Hosted Server, and Homey Cloud. You need at least Homey Mobile App v9.10.0 or higher in order to start updates.
{% endhint %}

## Getting Started

You can add firmware updates to your drivers. To do this, you'll need to gather the firmware files for your devices and create a `/drivers/<driver_id>/driver.firmware.compose.json` file. The `driver.firmware.compose.json` file contains metadata for Homey to be able to select the correct update for your devices.

For more information on the Zigbee Over-the-Air Upgrade cluster and process, read chapter 11 of the [Zigbee Cluster Specification](https://etc.athom.com/zigbee_cluster_specification.pdf).

{% hint style="info" %}
The `homey app driver firmware` Homey CLI command will take you through creating or updating the `driver.firmware.compose.json` file. (Requires `homey` v4.3.0 or higher.)
{% endhint %}

### File Structure

The `driver.firmware.compose.json` contains the following fields:

* `updates`: A list of all firmware updates for the devices of this driver.
* `wakeInstruction`: A description of how a Sleepy End Device (e.g battery-powered devices) can be made active by the user to start the firmware update. This field is not required for non-sleepy devices (takes a [Translation Object](/the-basics/app/internationalization)).

#### Updates

Each item in `updates` describes a firmware update for specific Zigbee devices. Every update contains the following fields:

* `changelog`: A brief description of the changes in this update (takes a [Translation Object](/the-basics/app/internationalization)).
* `device`: An object with the `manufacturerName` and `productId` combination that identifies the target device(s) for this update.
  * A driver can target multiple devices (see [Zigbee Manifest](/wireless/zigbee#manifest)). This field allows making a firmware update only available for a specific subset of those devices.
* `files`: A list of firmware update files included in this update.
  * In most cases, you'll only have a single file for your update.

#### Update Files

Each entry in `updates[].files` describes one firmware file. The following metadata fields are required for each file:

* `fileVersion`: The version of the firmware file. Higher values represent newer firmware. *(Numeric, as defined in the Zigbee cluster specification.)*
* `imageType`: A manufacturer-specific type that identifies the firmware image. *(Numeric, as defined in the Zigbee cluster specification.)*
* `manufacturerCode`: The assigned manufacturer code of the vendor. *(Numeric, as defined in the Zigbee cluster specification.)*
* `size`: The file size in bytes.
* `name`: The file name. The file itself must be stored in `/drivers/<driver_id>/assets/firmware/<file name>`.
* `integrity`: A hash of the file in the format `<hash_name>:<hex_encoded_hash>`.
  * The following hash types are supported: `sha256`, `sha384`, `sha512`, `sha512-256`, `sha3-256`, `sha3-384`, `sha3-512`, `blake2b512`, `blake2s256`.

{% hint style="info" %}
The `homey app driver firmware` command will automatically fill in these fields for you, and copy the firmware file to the correct location.
{% endhint %}

You can optionally add the following fields to limit when a firmware update is available:

* `minFileVersion`: The minimum file version the device must currently report. *(Numeric)*
* `maxFileVersion`: The maximum file version the device must currently report. *(Numeric)*
* `minHardwareVersion`: The minimum hardware version the device must report. *(Numeric)*
* `maxHardwareVersion`: The maximum hardware version the device must report. *(Numeric)*

{% hint style="info" %}
When you upload your Homey App, the firmware files are stored separately from your App. Only when Homey starts installing an update for a device, it will download the required files.
{% endhint %}

### Example

Putting everything together, a `driver.firmware.compose.json` file with a single update will look like this:

{% code title="/driver/\<driver\_id>/driver.firmware.compose.json" %}

```json
{
  "wakeInstruction": {
    "en": "Hold the internal button for three seconds."
  },
  "updates": [
    {
      "changelog": {
        "en": "- Fixes issue X\n- Adds feature Y\n- Deprecates feature Z"
      },
      "device": {
        "manufacturerName": "Company XYZ",
        "productId": ["AwesomeSensor", "AwesomeSensor Rev B"]
      },
      "files": [
        {
          "fileVersion": 1234,
          "imageType": 1000,
          "manufacturerCode": 5678,
          "size": 262144,
          "name": "AwesomeSensor_v1.3.4.bin",
          "integrity": "sha256:dac89981aeb5352a8ddce9fbb5ab3ad5bff88d17e2f7937cc90947301ccfedd2"
        }
      ]
    }
  ]
}
```

{% endcode %}

### Firmware Files

Homey Apps are responsible for delivering the correct firmware files to the appropriate devices. Before shipping a firmware update in your Homey App, ensure the following:

* **Device targeting:** Verify that each firmware file is mapped to the correct device(s). Delivering a firmware file to an incompatible device can cause irreversible damage or render it unresponsive.
* **End-to-end testing:** Test the full update flow on the intended physical hardware. Confirm that Homey can correctly initiate, transfer, and complete the firmware update process.
* **Validation before release:** Do not add a firmware update to your app until both of the above have been verified. Untested firmware updates should never reach end users.

## Update Selection

During the Zigbee OTA progress, a device will send a `queryNextImageRequest` command to Homey. In this command, the device provides Homey with the following information:

* `manufacturerCode`: The device's assigned manufacturer code
* `imageType`: Manufacturer-specific code
* `fileVersion`: The device's current version of the firmware
* (Optional) `hardwareVersion`: The device's current hardware version

Homey will select the firmware for your device based on the following:

1. The `updates[].device` field, must match the device's reported manufacturer name and product id.
2. One of the files in an `update` should match the device's current reported `manufacturerCode` and `imageType`.
3. If provided, the `min/maxFileVersion` and `min/maxHardwareVersion` should match the device's `fileVersion` and `hardwareVersion`.
4. The `fileVersion` of the matched file should be higher than the current reported `fileVersion` of the device.

If multiple updates are found for the device, the update with the highest `fileVersion` will be selected.

#### Multiple Firmware Files

Some devices might require multiple firmware files for a single update (e.g. when it also needs to update the firmware of another chip in the device). The device will do this by sending another `queryNextImageRequest` with a different `manufacturerCode`, `imageType` and `fileVersion`. Put secondary firmware upgrade files in the same `updates[].files` list to allow continuing the update without requiring the user to start the update.

## Update Failures

Firmware updates can fail. Homey considers an update failed in the following cases:

* **Explicit abort:** The device reports an error status during image transfer, actively aborting the update.
* **Stalled transfer:** The device stops requesting firmware chunks before the transfer is complete.
* **No reconnection:** The device fails to rejoin the Zigbee network within the expected timeframe after a file has been fully transferred.

In all failure cases, the user will be notified via the firmware update screen and can retry the installation.


# 433 MHz

Homey is capable of sending and receiving 433 MHz radio-signals for controlling remote appliances like wireless switches and thermostats.

Transmitting and receiving radio-signals requires a signal definition. This signal definition contains all elements necessary for Homey to properly receive and transmit radio signals.

Developing apps for radio controlled devices requires knowledge of data-encoding and signalling.

The `homey-rfdriver` library for Node.js implements the basic functionality needed for all 433 MHz apps and implements a higher level interface that you can use to control 433 MHz devices. Install `homey-rfdriver` with the following command:

```shell
npm install homey-rfdriver
```

Read the RFDriver documentation at <https://athombv.github.io/node-homey-rfdriver>.

{% hint style="info" %}
In order to use 433 MHz or 868 MHz\* signals your app will need the `homey:wireless:433` or `homey:wireless:868` permissions. For more information about permissions read the [permissions guide](/the-basics/app/permissions).

\*note that 868 MHz is only available on Homey Pro 2019 or earlier models.
{% endhint %}

{% hint style="info" %}
You can view a working example of a Homey App that uses 433 MHz at: <https://github.com/athombv/nl.klikaanklikuit-example>
{% endhint %}

## What are signals?

Signals are block-wave data signals carried over the air using an electromagnetic carrier on a given frequency. Homey can communicate on 433 MHz and 868 MHz frequency bands.

### Radio modulation

An electromagnetic field has the ability to change in amplitude (signal strength) and frequency. These characteristics can be used to modulate a data signal. When the amplitude of an electromagnetic field is used to represent a high or low state it is called 'amplitude shift keying' (ASK). When a change in frequency is used to modulate a signal it is called 'frequency shift keying' (FSK).

There is also a variation on ASK modulation used by most 433 MHz devices called 'on-off keying' (OOK). Instead of defining two amplitude levels for the high and low states, OOK modulation only uses a high state amplitude level. The low state amplitude level is represented by the absence of the carrier. This is a very easy and cheap way of modulating radio signals as it only requires a few hardware components that can generate and toggle an electromagnetic carrier.

### Receiving

Receiving electromagnetic waves depends on the antenna and filtering that is used. Antennas are designed to operate at a certain frequency, this prevents the reception of noisy and unwanted signals. In our case we only want to receive on the 433 MHz and 868 MHz bands, for which Homey contains two different antennas. Unfortunately using two separate antennas is not enough to prevent the reception of noise and unwanted signals. To ensure that a device only receives signals within a specific frequency band, it is important to filter out incoming signals at other frequencies.

However, in practice devices often use a somewhat deviated carrier-frequency (e.g. 433.89MHz). To ensure that Homey is able to listen to these deviating frequencies, it's receiving frequency band has been broadened by 325Khz. This results to a frequency band ranging from 433.76MHz to 434.08MHz for 433 MHz signals and 868.14MHz to 868.46MHz for 868 MHz signals.

Homey can only listen to devices with radio-frequencies that are within its frequency band

### Data encoding

To convert a received block-wave into usable data, we have to decode this wave. To do this, we have to define how the data is represented in the wave. This depends on the encoding mechanism a device uses. Some device manufacturers develop their own encoding mechanisms, while others use pre-defined standards. These encodings can be edge-based (eg manchester) or duration based (eg X10). Homey can be configured to operate in both modes.

## Signal definition

In order to do this, Homey needs to know the details of the protocol used. This description is called the signal definition. All signal definitions consist of two kind of properties, encoding-specific properties, and radio-specific configuration properties. Homey apps need to register their signal definition with the Homey Signal Manager. The Homey Signal Manager receives all incoming raw transmissions and attempts to match these block-waves to a registered signal definition. When an incoming signal matches a registered signal definition, the signal is automatically decoded and the contained data is routed to the corresponding app. To transmit, data travels the reverse path which means the data is encoded using the same signal definition and then transmitted.

The signal definition contains all the necessary attributes for sending and receiving signals.

## Obtaining a signal

It is possible to make Homey record raw data for a short period of time using the devtools, read the devtools guide for more information.

## Using signals in your App

Signals should be defined in your App Manifest, under `.homeycompose/signals/<frequency>/<signal_id>.json`, where `frequency` is either `433` or `868`.

{% code title="/.homeycompose/signals/433/klikaanklikuit.json" %}

```javascript
{
  "sof": [275, 2640], // Start of frame
  "eof": [275], // End of frame
  "words": [
    [250, 275, 250, 1250], // 0, or LOW
    [250, 1250, 250, 275] // 1, or HIGH
  ],
  "interval": 10000, // Time between two subsequent signal repetitions
  "sensitivity": 0.5, // between 0.0 and 0.5
  "repetitions": 20,
  "minimalLength": 32,
  "maximalLength": 36
}
```

{% endcode %}

Before your app can use this signal definition, it must be registered:

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  async onInit() {
    // create & register a signal using the id from your signal manifest
    const mySignal = this.homey.rf.getSignal433("my_signal");

    // start listening to data by enabling receive
    await mySignal.enableRX();

    // on a payload event
    mySignal.on("payload", function (payload, first) {
      console.log(`received data: ${payload} isRepetition ${!first}`);
    });

    // stop listening to data by disabling receive
    await mySignal.disableRX();

    // transmit the bits 01011001
    await mySignal.tx([0, 1, 0, 1, 1, 0, 0, 1]);

    // transmit predefined command
    await mySignal.cmd("ONOFF");
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/app.mts" %}

```mts
import Homey from "homey";

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    // create & register a signal using the id from your signal manifest
    const mySignal = this.homey.rf.getSignal433("my_signal");

    await mySignal.enableRX();

    // on a payload event
    mySignal.on("payload", function (payload: number[], first: boolean) {
      console.log(`received data: ${payload} isRepetition ${!first}`);
    });

    // stop listening to data by disabling receive
    await mySignal.disableRX();

    // transmit the bits 01011001
    await mySignal.tx([0, 1, 0, 1, 1, 0, 0, 1]);

    // transmit predefined command
    await mySignal.cmd("ONOFF");
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/app.py" %}

```python
from homey import device


class Device(device.Device):
    async def on_init(self) -> None:
        # create & register a signal using the id from your signal manifest
        my_signal = self.homey.rf.get_signal_433("my_signal")

        await my_signal.enable_rx()

        def on_payload(payload: tuple[int, ...], first: bool) -> None:
            self.log(f"received data: {payload} isRepetition ${not first}")

        my_signal.on_payload(on_payload)

        # stop listening to data by disabling receive
        await my_signal.disable_rx()

        # transmit the bits 01011001
        await my_signal.tx([0, 1, 0, 1, 1, 0, 0, 1])

        # transmit predefined command
        await my_signal.cmd("ONOFF")


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

{% hint style="info" %}
Please only register a signal if there are devices any paired, because receiving signals is quite performance intensive.
{% endhint %}

## Signal requirements

A signal has to meet certain requirements in order to guarantee a proper flow of sending and receiving signals. For example, if a signal is to long, it could block other apps from sending their signals. The table below shows the signal requirements.

| Signal characteristic    | Description                                                  | Minimal value | Maximal value |
| ------------------------ | ------------------------------------------------------------ | ------------- | ------------- |
| Converted time-intervals | Number of time-intervals that is used to generate the signal | 1             | 256           |
| Signal duration          | The total duration of a signal that is being sent            | 5us           | 1s            |

## Transmitting

After a signal definition has been made it can be used to transmit or receive data. Data is encoded based on a signal definition which at the end is nothing more than an array of time-intervals. The Homey Signal Service receives the array with time-intervals, configures the radio with the appropriate settings and generates the signal.

### Receive-after-transmit

Sometimes data only needs to be requested from a device. A request message is sent to the device and the device immediately response with the requested data. In order to receive these responses properly the radio has to switch to receive mode immediately after sending the request. With the *rxTimeout* attribute a receive timeout can be configured. The radio stays in receive mode for the configured time in milliseconds.

The Radio-controller cannot handle time-interval arrays larger then 256 intervals

## Receiver configuration

The table below shows the radio configuration used by the 433 MHz and 868 MHz receivers. The receiver's filter bandwidth in listening mode for both 433 MHz and 868 MHz can not be changed by a developer since it is shared by other Homey apps. Changing the filter bandwidth could lead to an unstable reception of these other signals.

### 433 MHz configuration

| Attribute         | Value       |
| ----------------- | ----------- |
| Carrier frequency | 433890000Hz |
| Channel spacing   | 325000Hz    |
| BaudRate          | 12004Bd     |
| Modulation        | ASK         |

### 868 MHz configuration

*only available on Homey Pro 2016-2019*

| Attribute         | Value       |
| ----------------- | ----------- |
| Carrier frequency | 868300000Hz |
| Channel spacing   | 325000Hz    |
| BaudRate          | 12004Bd     |
| Modulation        | ASK         |

## Creating a signal

A signal definition consists of two types of properties; signal encoding properties and radio configuration properties. The first part specifies the characteristics of the data encoding of a signal whereas the radio part specifies the required radio configuration to receive the signal.

The image below shows a repeating signal captured by a logic-analyzer from a `KlikAanKlikUit` remote. This signal will be used as an example throughout the article.

![](/files/-MYAgtq9DwmrIq3AH3dp)

Red: `start-of-frame` Yellow: `word 0` Green: `word 1` Blue: `end-of-frame` Purple: `interval`

The accompanying signal definition would therefore become:

{% code title="/.homeycompose/signals/433/klikaanklikuit.json" %}

```javascript
{
  "sof": [275, 2640], // Start of frame
  "eof": [275], // End of frame
  "words": [
    [250, 275, 250, 1250], // 0, or LOW
    [250, 1250, 250, 275] // 1, or HIGH
  ],
  "interval": 10000, // Time between two subsequent signal repetitions
  "sensitivity": 0.5, // between 0.0 and 0.5
  "repetitions": 20,
  "minimalLength": 32,
  "maximalLength": 36
}
```

{% endcode %}

The table below shows the minimum, maximum and default values of the encoding properties that can be used in the Homey signal definition.

| Attribute        | Description            | Min. | Default  | Max.         | Type                           | Unit         |
| ---------------- | ---------------------- | ---- | -------- | ------------ | ------------------------------ | ------------ |
| `agc`            | AGC pulses             | 5us  | -        | 32767us      | Array of integers              | Microseconds |
| `sof`            | Start-of-frame         | 5us  | -        | 32767us      | Array of integers              | Microseconds |
| `words`          | Words                  | 5us  | -        | 32767us      | Array of integers Array        | Microseconds |
| `eof`            | End-of-frame           | 5us  | -        | 32767us      | Array of integers              | Microseconds |
| `interval`       | Interval               | 5us  | 5000us   | 32767us      | Integer                        | Microseconds |
| `manchesterUnit` | ManchesterUnit         | 5us  | -        | 32767us      | Integer                        | Microseconds |
| `minimalLength`  | Minimal payload length | 1    | 1        | Infinity     | Integer                        | -            |
| `maximalLength`  | Maximal payload length | 1    | Infinity | Infinity     | Integer                        | -            |
| `prefixData`     | Prepended data         | 0    | -        | words.length | Array of Integers              | -            |
| `postfixData`    | Suffixed data          | 0    | -        | words.length | Array of Integers              | -            |
| `cmds`           | Static commands        | -    | -        | -            | String => Integer Array Object | -            |
| `toggleSof`      | Toggled SOF            | 5us  | -        | 32767us      | Array of integers              | Microseconds |
| `toggleBits`     | Toggle bit indexes     | 0    | -        | words.length | Array of Integers              | -            |
| `sensitivity`    | Sensitivity            | 0.0  | 0.3      | 0.5          | float                          | -            |
| `packing`        | Packing                | -    | false    | -            | Boolean                        | -            |
| `txOnly`         | Disable receiving      | -    | false    | -            | Boolean                        | -            |

### Automatic Gain Control pulse (AGC)

The `AGC` attribute defines the synchronisation sequence of a signal in time-intervals. This part of the signal never changes and is used by most devices to properly configure the gain of the radio receiver, before the data arrives. The sequence is defined by an array of time-intervals in microseconds. The AGC pulse is ignored when receiving, but added in transmitted signals.

It is also possible to use Manchester encoding. In that case the agc-array has to be filled with 1's and 0's rather then using time-intervals. Each `1` is replaced by a `ManchesterUnit` high interval and each `0` is replaced by a `ManchesterUnit` low interval. This interval can be defined in the `ManchesterUnit` attribute.

### Start-of-frame (SOF)

The `start-of-frame` attribute defines the preamble sequence of a signal in time-intervals. This part of the signal never changes and is used by most devices to detect an incoming signal. The sequence is defined by an array of time-intervals in microseconds

The start-of-frame definition in the example above starts with a high-state interval of 275 microseconds followed by a low-state interval of 2640 microseconds. The `sof` is shown in the signal image indicated in red.

{% hint style="warning" %}
When using Manchester encoding the word-array has to be filled with 1's and 0's rather then using time-intervals.
{% endhint %}

### Words

The `words` attribute contains one or more words. Each word defines a sequence of high and low time-intervals depending on the type of encoding that is used. Most devices use single-level encoding where one word defines the `1` bit and the other word defines the `0` bit. In case of multi-level encoding the bitmask increases from one bit to multiple bits. Each bit combination (00, 11, 10, 01) then refers to a particular word.

In the above example, two words are defined. The `0` word indicated in yellow corresponds to a 0-bit. The `1` word indicated in green corresponds to a 1-bit. Therefore, the data that has been sent in the example will be `00111111010001010100110110010000` summing up to 32 bits. In the 'klik-aan-klik-uit' app these bits are further split into multiple parts representing the homecode, unitcode and dim/onoff value.

When using Manchester encoding the word-array has to be filled with 1's and 0's rather then using time-intervals.

Data is sent and received using an array of binary (bit) values. Each bit (or bits when using multi-level encoding) refers to one of the words defined in the signal definition. When sending data the array of bits gets converted to the corresponding words.

### End-of-frame (EOF)

The `end-of-frame` attribute defines the end-sequence of a signal. This part of the signal never changes. Make sure to specify `minimalLength`When the EOF (partially) overlaps with the words, as this increases the amount of proper matches. When looking at the example, the end-of-frame consists of only one 275 microseconds interval. This is clearly shown in the signal-image indicated in blue.

When using Manchester encoding the eof-array has to be filled with 1's and 0's instead of a time-interval.

### Interval

The `interval` attribute defines the time-interval between subsequent signals. The value is specified in microseconds.

### ManchesterUnit

This time-interval is used when Manchester encoding is enabled. Instead of defining words in time-intervals, the words need to be defined in bits (1's and 0's). A word like \[1, 0] with a Manchester unit time-interval of 100 microseconds gets encoded into a high signal of 100 microseconds and a low signal of 100 microseconds.

### MinimalLength

The `minimalLength` attribute defines the minimal length of a signal in words. Sometimes devices use varying signal lengths depending on the size of the payload.

### MaximalLength

The `maximalLength` attribute defines the maximal length of a signal in words. Some devices use varying signal lengths depending on the size of the payload.

### PrefixData

The `PrefixData` attribute can be used to specify words that have to be added in front of each payload before transmission. When specified, incoming payloads are checked for these prefix-words, and only upon a match, the words will be stripped and the payload without prefix will be delivered to the Homey app. When transmitting data, this prefix is added to each payload as well.

### PostfixData

The `PostfixData` attribute can be used to specify words that have to be appended after each payload before transmission. When specified, incoming payloads are checked for these postfix-words, and only upon a match, the words will be stripped and the payload without postfix will be delivered to the Homey app. The words are also appended to each payload before transmitting as well.

### Commands

Sometimes, devices have a static set of commands and do not include any dynamic data such as an address. In these cases, the `cmds` attribute can be used to predefine all commands. This property consists of an object that maps commands (specified as identifier string) to an actual payload array (excluding prefix/postfix data).

### Toggle SOF

Some signals use toggle bits. These toggle bits are used to differentiate between multiple key presses of the same command or button, by flipping the bits upon each new transmission. Homey-Signals contain two different ways to specify this behaviour.

The `toggleSOF` property can be used to specify a custom SOF that is being alternated with the primary SOF. This works automatically for both receiving and sending, and adjusts the `first` argument of both the `cmd` and `payload` events.

### Toggle Bits

Some signals use toggle bits. These toggle bits are used to differentiate between multiple key presses of the same command or button, by flipping the bits upon each new transmission. Homey-Signals contain two different ways to specify this behaviour.

The `toggleBits` property can be used to specify bit indexes of these toggle bits inside the payload (including prefix/postfix). This works automatically for both receiving and sending, and adjusts the `first` argument of both the `cmd` and `payload` events. Note: The reported payload contains unmodified data, with the toggle bits as transmitted.

### Sensitivity

The `sensitivity` attribute defines the maximum deviation between the signal definition and the received signal. This value represents the maximal percentage signals may deviate of the definition.

### Packing

When sending or receiving large amounts of data it may be more convenient to receive the data in bytes rather than bits. Enabling `packing` makes it possible to send and receive byte- rather then bit-arrays. Every eight bits are 'packed' into a byte and added to the byte-array. Usage of this property is discouraged when the payload length is not a multiple of eight, and impossible when the signal does not contain exactly two words.

### txOnly

The `txOnly` attribute can be used to disable the receiving subsystem of the signal, proper usage is encouraged in order to save computing resources.

## Radio Configuration

The table below shows the minimum, maximum and default values of the attributes in the signal definition

| Attribute                     | Description       | Min.           | Default        | Max.           | Type    | Unit         |
| ----------------------------- | ----------------- | -------------- | -------------- | -------------- | ------- | ------------ |
| `repetitions`                 | Repetitions       | 1              | 10             | 255            | Integer | -            |
| `rxTimeout`                   | rxTimeout         | 0              | 10             | 255            | Integer | Milliseconds |
| `modulation.type`             | Modulation        | -              | 'ASK'          | -              | String  | -            |
| `modulation.channelSpacing`   | Channel spacing   | 58000          | 325000         | 812000         | Integer | Hertz        |
| `modulation.channelDeviation` | Channel deviation | 5000           | 25000          | 50000          | Integer | Hertz        |
| `modulation.baudRate`         | baudRate          | 1000           | 12004          | 200000         | Integer | Baud (Bd)    |
| `carrier`                     | Carrier frequency | radio specific | radio specific | radio specific | Integer | Hertz        |

These are the `carrier` frequencies for 433 MHz:

min: 433000000, default: 433920000, max: 433990000

These are the `carrier` frequencies for 868 MHz:

min: 868000000, default: 868300000, max: 868990000

### Repetitions

The `repetitions` attribute defines how often the signal gets transmitted. A repetitions value of 1 means 1 transmit, 2 means 2 transmits etc.

There are three ways to set the number of repetitions, from high to low priority:

* `Signal.tx()` repetitions parameter (see [SDK v3 reference](https://apps-sdk-v3.developer.homey.app/Signal.html#tx)).
* Signal definition repetitions-attribute (see [Signal definition](#signal-definition)).
* When non of the above are specified, it will default to 20.

> The repetition behavior was aligned between different Homey products as of Homey Pro (2016—2019) v10.0.6, Homey Pro (Early 2023) v10.3.1 and Homey Bridge v85.

### RxTimeout

Receiver timeout when sending requests to a device. After the data has been sent, the radio switches into receiving mode and waits for `rxTimeout` milliseconds for the device to respond. The same radio configuration used when sending the data is also used when waiting for the response.

### Modulation

The `modulation` attribute defines the modulation used by the radio to modulate the signal. When no modulation attribute is provided, Homey will use the default configuration.

#### Type

The `type` attribute defines the type of radio modulation that is used by the radio. Supported values are `ASK`, `FSK` and `GFSK`

#### Channel spacing

The `channelSpacing` attribute defines the filter bandwidth in receive mode. This attribute is only used when `rxTimeout` is greater than zero.

#### Channel deviation

The `channelDeviation` attribute defines the frequency deviation when using FSK or GFSK modulation.

#### Baudrate

The `baudRate` attribute defines the symbol changes per second of the specified signal in Baud.

### Carrier

The `carrier` attribute defines the carrier frequency used while sending a signal. Most radio-devices use the 433.92MHz and 868.3MHz as carrier frequencies. There are devices that use deviating carrier frequencies (e.g. Somfy). If no carrier attribute is defined, the default receiving carrier frequency will be used.


# Infrared

You can create Homey apps that use Infrared signals to control devices. This allows you to automate devices such as TVs and speakers that don't have "smart" capabilities.

Infrared is a `Signal` similar to [Radio (433Mhz & 868Mhz)](/wireless/rf-433mhz-868mhz), this means that adding support for an Infrared device to your Homey App is done in much the same way as adding a 433Mhz device.

{% hint style="info" %}
In order to use Infrared in your app will need the `homey:wireless:ir`permission. For more information about permissions read the [permissions guide](/the-basics/app/permissions).
{% endhint %}

{% hint style="info" %}
You can view a working example of a Homey App that uses Infrared at: <https://github.com/athombv/com.lg.ir-example>
{% endhint %}

## Installation

The `homey-rfdriver` library for Node.js implements the basic functionality needed for all infrared apps and implements a higher level interface that you can use to control infrared devices. Install `homey-rfdriver` with the following command:

```bash
npm install homey-rfdriver
```

You will also need to copy the infrared pairing templates from [node-homey-rfdriver.](https://github.com/athombv/node-homey-rfdriver/tree/master/pair)

Read the RFDriver documentation at <https://athombv.github.io/node-homey-rfdriver>.

Copy the [`pair/rf_ir_remote_add.html`](https://github.com/athombv/node-homey-rfdriver/blob/master/pair/rf_ir_remote_add.html) and [`pair/rf_ir_remote_learn.html`](https://github.com/athombv/node-homey-rfdriver/blob/master/pair/rf_ir_remote_learn.html) files to the `drivers/<driver_id>/pair/` folder of your app.

## Usage

Now that you have `homey-rfdriver` installed and copied the pairing templates, you can add them to your driver's manifest:

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "TV" },
  "class": "tv",
  "capabilities": ["onoff", "channel_up", "channel_down"],
  "images": {
    "small": "/drivers/my_driver/assets/images/small.jpg",
    "large": "/drivers/my_driver/assets/images/large.jpg"
  },
  "infrared": {
    "satelliteMode": true
  },
  "pair": [
    {
      "id": "rf_ir_remote_learn",
      "navigation": {    "next": "rf_ir_remote_add" },
      "options": {
        "title": { "en": "Pair your IR remote" },
        "instruction": { "en": "Press next to pair your remote." }
      }
    },
    {
      "id": "rf_ir_remote_add"
    }
  ]
}
```

{% endcode %}

### Signal definition

Next you need to add the signals you want to send to your manifest, you can define your signals by creating `.homeycompose/signals/ir/my_signal.json`. We will explain the basics next but if you want to know more about creating signal definitions your should read the [RF 433Mhz/868Mhz guide](/wireless/rf-433mhz-868mhz).

First you need to configure the carrier frequency, number of repetitions for all commands and the signal characteristics. For example:

{% code title="/.homeycompose/signals/ir/my\_signal.json" %}

```javascript
{
  "carrier": 37900,
  "sof": [4535, 4465],
  "eof": [590],
  "words": [
    [590, 590],
    [590, 1690]
  ],
  "prefixData": [1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0],
  "interval": 1000,
  "sensitivity": 0.5,
  "repetitions": 5,
  "minimalLength": 32,
  "maximalLength": 32,
  "cmds": {}
}
```

{% endcode %}

The `carrier` frequencies for infrared are: min: 30000, default: 38000, max: 45000

Now that the signal characteristics are defined you can add commands, for example:

{% code title="/.homeycompose/signals/ir/my\_signal.json" %}

```javascript
  "cmds": {
    "POWER_ON": [1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0],
    "POWER_OFF": [0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0],
    "CHANNEL_UP": [0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1],
    "CHANNEL_DOWN": [0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1]
  }
```

{% endcode %}

### Classes

The only things left to do is to define the right classes, starting with the signal which tells `homey-rfdriver` what signal to use (and allows you to override the signal functionality):

{% code title="/drivers/\<my\_driver>/signal.js" %}

```javascript
const { RFSignal } = require('homey-rfdriver');

class MySignal extends RFSignal {
  static FREQUENCY = 'ir';
  static ID = 'my_signal';
}

module.exports = MySignal;
```

{% endcode %}

Next you need to link create a `Driver` that extends `RFDriver` and the link your `RFSignal` class to it.

{% code title="/drivers/\<my\_driver>/driver.js" %}

```javascript
const { RFDriver } = require('homey-rfdriver');
const MySignal = require('./signal.js');

class MyDriver extends RFDriver {
  static SIGNAL = MySignal;
}

module.exports = MyDriver;
```

{% endcode %}

And lastly you need to create a `Device` that extends `RFDevice` and create a mapping from Homey capabilities to the commands you defined for your signal.

{% code title="/drivers/\<my\_driver>/device.js" %}

```javascript
const { RFDevice } = require('homey-rfdriver');

class MyDevice extends RFDevice {
    static CAPABILITIES = {
    onoff: {
      'true': 'POWER_ON',
      'false': 'POWER_OFF',
    },
    channel_up: 'CHANNEL_UP',
    channel_down: 'CHANNEL_DOWN',
  }
}

module.exports = MyDevice;
```

{% endcode %}

## Prontohex

Homey also supports Prontohex signal definitions for infrared devices.

### Properties

The table below shows the encoding properties in the Prontohex signal definition.

| Attribute | Description     | Remark                                      |
| --------- | --------------- | ------------------------------------------- |
| `cmds`    | Static commands | The Prontohex command string definition     |
| `type`    | Type of Signal  | Set to `prontohex` to enable prontohex mode |

### Commands

Sometimes, devices have a static set of commands and do not include any dynamic data such as an address. This is the case for most infrared devices. In these cases, the `cmds` attribute can be used to predefine all commands. This property consists of an object that maps commands (specified as identifier string) to a prontohex payload (specified as String). The carrier in the prontohex String overrides the carrier in the signal radio specification.

{% code title="/.homeycompose/signals/ir/my\_signal.json" %}

```javascript
{
  "type": "prontohex",
  "cmds": {
    "ON": "0000 0073 0000 000D 0020 0020 0040 0020 0020 0020 0020 0020 0020 0020 0020 0020 0020 0040 0020 0020 0020 0020 0020 0020 0020 0020 0020 0020 0020 0CA4",
    "OFF": "0000 0073 0000 000C 0020 0020 0040 0020 0020 0020 0020 0020 0020 0020 0020 0020 0020 0040 0020 0020 0020 0020 0020 0020 0040 0040 0020 0CA4"
  }
}
```

{% endcode %}

{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  async onInit() {
    const philipsSignal = this.homey.rf.getSignalInfrared('philips');

    await philipsSignal.cmd('ON');
  }
}

module.exports = App;
```

{% endcode %}


# Matter

Matter is a smart-home communication protocol. It is build upon Wi-Fi, Ethernet and Thread. It is available on Homey Pro (Early 2023, 2026, mini) and Homey Self-Hosted Server.

{% hint style="warning" %}
Matter apps are supported since Homey Pro (Early 2023) v11.1.0.
{% endhint %}

Matter is a smart-home protocol that was released by the Connectivity Standards Alliance in 2022. The standard describes how the Matter protocol functions and how devices using Matter should react to certain commands. Because of this, Homey Pro can control all Matter devices without the need of a Homey App. However, Homey apps can enhance the experience of a Matter device by adding pairing instructions and device icons.

## Creating a Matter Driver

To create a matter driver you'll need to add a driver manifest file in `/drivers/<driver_id>/driver.compose.json`. This file will describe your Matter device. You will need to add at least the following fields in this file (also add the required fields described in [Drivers & Devices](/the-basics/devices#driver-manifest) on the Drivers & Devices page).

* `platforms`: Always specify "local" here. Matter is not available on Homey Cloud.
* `connectivity`: Always specify "matter" here. This is what makes your driver a driver for a Matter device.
* `class`: Use a device class that best fits your device.
* `capabilities`: Set the capabilities your device has (you can find the capabilities in the developer tools after you have added your device to Homey Pro).
* `matter`: An object that describes some specific properties for Matter.
  * `vendorId`: The vendor id that the Matter device uses (number or number array to support multiple devices with a single driver).
  * `productId`: The product id that the Matter device uses (number or number array to support multiple devices with a single driver).

{% hint style="info" %}
Note that the `class` and `capabilities` fields are only used to display the Matter device in the Homey App Store. When a Matter device is added to Homey Pro it will automatically determine which capabilities and device class are suited for it, and the capabilities and device class of the driver manifest are ignored.
{% endhint %}

{% hint style="info" %}
You can find the vendor id and product id of a Matter device by checking the advanced settings of the device after adding it to Homey. Note that the advanced settings show a vendor and product id in hexadecimal (e.g. `0x1234`), but you can only put a base-10 number in your `driver.compose.json` (e.g. `4660`).
{% endhint %}

### Driver and Device

Because it is only possible to provide icons and pair instructions for a Matter device you cannot provide a custom `Driver` or `Device` class for the device. Homey Pro will take care of handling all capabilities and device updates.

### Adding Pair Instructions

To make it easier for a user to connect a Matter device to Homey Pro, you can add pairing instructions to your driver. These instructions should tell the user how to put the Matter device into pairing mode. To add pairing instructions, add a field `learnmode` to the `matter` field of your `driver.compose.json`. It has the following fields:

* `instruction`: A translation object that tells how to enable the pairing mode on the device.
* `image`: An image (or animated SVG) that shows how to enable the pairing mode on the device (optional).

### Adding An Icon

Refer to [Drivers & Devices](/the-basics/devices#icon) on how to add an icon to your driver.

### Example Manifest

Below you see an example `driver.compose.json` file for a Matter device.

{% code title="/drivers/\<driver\_id>/driver.compose.json" lineNumbers="true" %}

```json
{
  "name": { "en": "My Driver" },
  "platforms": ["local"],
  "connectivity": ["matter"], 
  "class": "socket",
  "capabilities": ["onoff", "dim"],
  "matter": {
    "vendorId": 1234,
    "productId": 4567,
    "learnmode": {
      "instruction": { "en": "Press the button on your device three times" },
      "image": "/drivers/<driver_id>/assets/learnmode.svg"
    }
  }
}
```

{% endcode %}

## Bridged Devices

The Matter specification also defines a Matter bridge. This is a device that exposes other non-Matter devices through the Matter protocol. It is possible to create a driver for a bridged Matter device.

{% hint style="info" %}
For example: a manufacturer of a Zigbee hub can add support for Matter to its hub. The hub can then expose all Zigbee devices through the Matter protocol. The hub will convert the commands that are send through Matter to commands that can be send over Zigbee to the Zigbee devices.
{% endhint %}

To add a driver for a Matter bridge to your Homey App, you will need at least two drivers:

* A driver for the bridge itself. This driver can be selected by the user and should provide instructions on how to put the Matter bridge into pairing mode. The bridge itself will never be added as a device to Homey.
* Drivers for the bridged devices. Each driver can provide an icon for the bridged device.

### Matter Bridge Manifest

The manifest for the Matter Bridge should contain the vendor id and product id of the Matter bridge itself. It should not contain any capabilities and the device class should be `"bridge"`. The Matter bridge is the only device that can be selected by the user when adding a Matter device.

#### Example Matter Bridge Manifest

{% code title="/drivers/\<bridge\_driver\_id>/driver.compose.json" lineNumbers="true" %}

```json
{
  "name": { "en": "My Bridge Driver" },
  "platforms": ["local"],
  "connectivity": ["matter"], 
  "class": "bridge",
  "capabilities": [],
  "matter": {
    "vendorId": 1234,
    "productId": 4567,
    "learnmode": {
      "instruction": { "en": "Press the button on your device three times" },
      "image": "/drivers/<bridge_driver_id>/assets/learnmode.svg"
    }
  }
}
```

{% endcode %}

### Bridged Device Manifest

For each bridged device you should add an additional driver. This driver is used to determine the icon of the device. The manifest of a bridged Matter device should contain two additional properties in the `matter` object. These properties are based on the attributes of the `Bridged Device Basic Information` cluster for the bridged device.

* `deviceVendorId`: The `VendorID` attribute reported by the bridged Matter device (number or number array to support multiple devices with a single driver).
  * If no `VendorID` attribute is present, use the `VendorID` property of the `Basic Information` cluster of the root endpoint.
* `deviceProductName`: The `ProductName` attribute that is reported by the bridged Matter device (string or string array to support multiple devices with a single driver).

You will also need to keep the `vendorId` and `productId` of the Matter bridge in the manifest. This tells Homey to which Matter bridge this device belongs.

{% hint style="warning" %}
The `ProductName` is an optional attribute of the `Bridged Device Basic Information` cluster. If it is not present, it is not possible to create a Homey driver for this bridged device.
{% endhint %}

{% hint style="info" %}
You can find the attributes of the `Bridged Device Basic Information` cluster by adding the Matter Bridge to Homey. Then go to the [Matter Developer Tools](https://tools.developer.homey.app/tools/matter) and perform an interview of the Matter Bridge. This will show all attributes for all endpoints the bridge has.
{% endhint %}

#### Example Bridged Device Manifest

{% code title="/drivers/\<bridged\_device\_driver\_id>/driver.compose.json" lineNumbers="true" %}

```json
{
  "name": { "en": "My Bridged Device Driver" },
  "platforms": ["local"],
  "connectivity": ["matter"], 
  "class": "other",
  "capabilities": [],
  "matter": {
    "vendorId": 1234,
    "productId": 4567,
    "deviceVendorId": 1234,
    "deviceProductName": "XYZ-123"
  }
}
```

{% endcode %}

## Platform Local Required Feature

You could add `matter` to the `platformLocalRequiredFeatures` array of your app manifest (refer to [Manifest](/the-basics/app/manifest#platform-local-required-features)). This will make it impossible to install the app on a Homey Pro that does not have support for Matter.

{% hint style="info" %}
Adding Matter to the `platformLocalRequiredFeatures` array is recommended when your app only contains drivers for Matter devices. If your app has drivers for other technologies or other features, Matter should not be added to the required features list.
{% endhint %}

## Firmware Updates

Homey will periodically check the [Matter Distributed Compliance Ledger](https://webui.dcl.csa-iot.org/models) (DCL) for updates of Matter devices. To make an update available for a Matter device, make sure your update is added to the DCL.


# OAuth2

Authorise Homey to use a Web API with OAuth2.

## Introduction

OAuth2 is an authentication standard used often by smart home manufacturers for delegating user access to their Web API. You might have previously encountered this as a "Candy Crush wants access to your Facebook account"-type dialog.

Usually, a developer registers an OAuth2 client on a developer-specific website owned by the manufacturer. Often a *Name*, *Redirect URL*, *Scopes* and/or an *Image* have to be provided.

![Homey's OAuth2 Consent Dialog](/files/-MZrkIMhAQPuZ54VNtNt)

{% tabs %}
{% tab title="JavaScript" %}
Almost always, the Redirect URL has to be entered beforehand for security reasons. Homey is behind a NAT, however, and thus does not have a static URL available to redirect to. For this scenario, you can use [`ManagerCloud#createOAuth2Callback()`](https://apps-sdk-v3.developer.homey.app/ManagerCloud.html#createOAuth2Callback). This method generates a unique URL to redirect the user to, which passes the resulting `code` parameter —which you can then swap for an access token— back to your app.
{% endtab %}

{% tab title="TypeScript" %}
Almost always, the Redirect URL has to be entered beforehand for security reasons. Homey is behind a NAT, however, and thus does not have a static URL available to redirect to. For this scenario, you can use [`ManagerCloud#createOAuth2Callback()`](https://apps-sdk-v3.developer.homey.app/ManagerCloud.html#createOAuth2Callback). This method generates a unique URL to redirect the user to, which passes the resulting `code` parameter —which you can then swap for an access token— back to your app.
{% endtab %}

{% tab title="Python" %}
Almost always, the Redirect URL has to be entered beforehand for security reasons. Homey is behind a NAT, however, and thus does not have a static URL available to redirect to. For this scenario, you can use [`ManagerCloud#create_oauth2_callback()`](https://python-apps-sdk-v3.developer.homey.app/manager/cloud.html#homey.manager.cloud.ManagerCloud.create_oauth2_callback). This method generates a unique URL to redirect the user to, which passes the resulting `code` parameter —which you can then swap for an access token— back to your app.
{% endtab %}
{% endtabs %}

{% hint style="info" %}
You can view working examples of Homey Apps that use OAuth2 at: <https://github.com/athombv/nl.thermosmart-example>, <https://github.com/athombv/nl.eneco.toon-example>, and\
<https://github.com/athombv/io.nuki-example>
{% endhint %}

## Homey OAuth2App

<div align="left"><img src="https://img.shields.io/npm/v/homey-oauth2app?label=homey-oauth2app" alt=""></div>

The recommended way to create a Homey app for an OAuth2 Web API is by using [homey-oauth2app](https://athombv.github.io/node-homey-oauth2app).

This module does all the heavy lifting related to OAuth2, such as logging in, obtaining an access token, refreshing tokens and making API calls.

Because no API is the same, the module has been designed specifically to be extended to fit your Web API. Even if your device's Web API differs from the [OAuth2 specification](https://tools.ietf.org/html/rfc6749), methods can be overloaded to change behaviour.


# Webhooks

Subscribe to incoming events with webhooks.

A webhook is an API concept that allows manufacturer's Web APIs to send realtime updates using regular HTTP requests.

Because Homey connects to the internet through a router, your app is not publicly accessible from the internet. We provide a webhook-forwarding service to route all incoming webhooks to the right Homey.

![](/files/-MZsHOMu8rtx1HLTzvEw)

#### Example

Let's say there is a Web API that sends a webhook when the user has turned on a light bulb. The webhook HTTP request might look like this:

```http
POST /webhook/56db7fb12dcf75604ea7977d HTTP/1.1
Host: webhooks.athom.com
Content-Type: application/json; charset=utf-8

{
  "device_id": "aaabbbccc",
  "turned_on": true
}
```

## 1. Creating a Webhook

First, you need a unique Webhook URL to provide to the manufacturer's Web API. Sometimes this can be configured in a manufacturer-owned developer portal, other times this URL can be registered by making an API call.

Go to <https://tools.developer.homey.app/webhooks> and select `New Webhook`. Copy the ID & Secret to your app's `/env.json` file as `WEBHOOK_ID` and `WEBHOOK_SECRET`.

```javascript
{
  "WEBHOOK_ID": "56db7fb12dcf75604ea7977d",
  "WEBHOOK_SECRET": "2uhf83h83h4gg34..."
}
```

## 2. Registering your Webhook

Secondly, in your app you you need to register a webhook listener that subscribes to this webhook.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  async onInit() {
    const id = Homey.env.WEBHOOK_ID; // "56db7fb12dcf75604ea7977d"
    const secret = Homey.env.WEBHOOK_SECRET; // "2uhf83h83h4gg34..."
    const data = {
      // Provide unique properties for this Homey here
      deviceId: 'aaabbbccc',
    };

    const myWebhook = await this.homey.cloud.createWebhook(id, secret, data);

    myWebhook.on('message', args => {
      this.log('Got a webhook message!');
      this.log('headers:', args.headers);
      this.log('query:', args.query);
      this.log('body:', args.body);
    });
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/app.mts" %}

```mts
import Homey from "homey";

type WebhookMessageArgs = {
  body: unknown;
  headers: Record<string, string>;
  query: Record<string, string>;
};

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    const id: string = Homey.env.WEBHOOK_ID; // "56db7fb12dcf75604ea7977d"
    const secret: string = Homey.env.WEBHOOK_SECRET; // "2uhf83h83h4gg34..."
    const data = {
      // Provide unique properties for this Homey here
      deviceId: "aaabbbccc",
    };

    const myWebhook = await this.homey.cloud.createWebhook(id, secret, data);

    myWebhook.on("message", (args: WebhookMessageArgs) => {
      this.log("Got a webhook message!");
      this.log("headers:", args.headers);
      this.log("query:", args.query);
      this.log("body:", args.body);
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/app.py" %}

```python
import homey
from homey import app
from homey.cloud_webhook import WebhookMessage


class App(app.App):
    async def on_init(self) -> None:
        id = homey.env["WEBHOOK_ID"]  # "56db7fb12dcf75604ea7977d"
        secret = homey.env["WEBHOOK_SECRET"]  # "2uhf83h83h4gg34..."
        data = {
            # Provide unique properties for this Homey here
            "deviceId": "aaabbbccc",
        }

        my_webhook = await self.homey.cloud.create_webhook(id, secret, data)

        def on_message(message: WebhookMessage) -> None:
            self.log("Got a webhook message!")
            self.log("headers:", message.get("headers"))
            self.log("query:", message.get("query"))
            self.log("body:", message.get("body"))

        my_webhook.on_message(on_message)


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}

## 3. Set-up your webhook URL

But how does the webhook service know that only this Homey may receive the webhook? There are three options, this is depending on how you register webhooks with the manufacturer's Web API.

### Option 1 — Dynamic webhooks using Query Parameters

If the manufacturer's Web API allows you to dynamically register a webhook, for example by posting your webhook URL to an endpoint (`POST https://myapi.com/webhook`), you can attach Homey ID as query parameter `homey` to the webhook.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/app.js" %}

```javascript
const Homey = require('homey');
const fetch = require('node-fetch');

class App extends Homey.App {
  async onInit() {
    const homeyId = await this.homey.cloud.getHomeyId();
    const webhookUrl = `https://webhooks.athom.com/webhook/${Homey.env.WEBHOOK_ID}?homey=${homeyId}`;
    
    await fetch('https://myapi.com/webhook', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({ url: webhookUrl }),
    });
  }
}

module.exports = App;
```

{% endcode %}

Homey's webhook forwarding service understands that webhooks with a `homey` query parameter should be forwarded to that Homey.

When using query parameters, the `data` property is not required when registering your webhook using the `this.homey.cloud.createWebhook()` function.
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/app.mts" %}

```mts
import Homey from "homey";
import fetch from "node-fetch";

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    const homeyId = await this.homey.cloud.getHomeyId();
    const webhookUrl = `https://webhooks.athom.com/webhook/${Homey.env.WEBHOOK_ID}?homey=${homeyId}`;

    await fetch("https://myapi.com/webhook", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ url: webhookUrl }),
    });
  }
}

```

{% endcode %}

Homey's webhook forwarding service understands that webhooks with a `homey` query parameter should be forwarded to that Homey.

When using query parameters, the `data` property is not required when registering your webhook using the `this.homey.cloud.createWebhook()` function.
{% endtab %}

{% tab title="Python" %}
{% code title="/app.py" %}

```python
import requests
from homey import app
from homey.homey import Homey


class App(app.App):
    async def on_init(self) -> None:
        homey_id = await self.homey.cloud.get_homey_id()
        webhook_url = f"https://webhooks.athom.com/webhook/${Homey.env.WEBHOOK_ID}?homey={homey_id}"

        requests.post(
            webhook_url,
            json={"url": webhook_url},
        )


homey_export = App

```

{% endcode %}

Homey's webhook forwarding service understands that webhooks with a `homey` query parameter should be forwarded to that Homey.

When using query parameters, the `data` property is not required when registering your webhook using the `self.homey.cloud.create_webhook()` function.
{% endtab %}
{% endtabs %}

### Option 2 — Webhooks using Key Path properties

If the manufacturer's Web API requires you to specify the webhook URL beforehand, for example in their developer portal, then you can use the *key path* option when creating your webhook.

The *key path* describes which property from the webhook request contains the value that uniquely identifies a Homey. For example: `headers['X-Device-Id']`, note that these properties are case-sensitive. You can use `body`, `headers` and `query` in your Webhook *key path* filter.

{% hint style="info" %}
A *key path* is an ECMAScript expression consisting only of identifiers (`myVal`), member accesses (`foo.bar`) and key lookup with literal values (`arr[0]` `obj['str-value'].bar.baz`).
{% endhint %}

{% tabs %}
{% tab title="JavaScript" %}
Your *key path* has to match an incoming webhook against the `data` object provided in `this.homey.cloud.createWebhook(id, secret, data)`. For example, an object with `{ $key: "aaabbbccc" }` has to match the value `aaabbbccc` in the `data` object.

It is also possible to define the keypath as an array, for example: `{ $keys: ["aaa", "bbb"] }` then the value in the *key path* either has to match value `aaa` or `bbb`.

The *key path* will be checked for each Homey that is registered to this webhook. All Homeys with matching data will receive the webhook in the `webhook.on('message', ...)` listener.
{% endtab %}

{% tab title="TypeScript" %}
Your *key path* has to match an incoming webhook against the `data` object provided in `this.homey.cloud.createWebhook(id, secret, data)`. For example, an object with `{ $key: "aaabbbccc" }` has to match the value `aaabbbccc` in the `data` object.

It is also possible to define the keypath as an array, for example: `{ $keys: ["aaa", "bbb"] }` then the value in the *key path* either has to match value `aaa` or `bbb`.

The *key path* will be checked for each Homey that is registered to this webhook. All Homeys with matching data will receive the webhook in the `webhook.on('message', ...)` listener.
{% endtab %}

{% tab title="Python" %}
Your *key path* has to match an incoming webhook against the `data` object provided in `self.homey.cloud.create_webhook(id, secret, data)`. For example, an object with `{ "$key": "aaabbbccc" }` has to match the value `aaabbbccc` in the `data` object.

It is also possible to define the keypath as an array, for example: `{ "$keys": ["aaa", "bbb"] }` then the value in the *key path* either has to match value `aaa` or `bbb`.

The *key path* will be checked for each Homey that is registered to this webhook. All Homeys with matching data will receive the webhook in the `webhook.on("message", ...)` listener.
{% endtab %}
{% endtabs %}

#### Example 1 — Headers

Let's use `headers['X-Device-Id']` from the example above, and define `$key` as an array in the `data` object, as follows:

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  async onInit() {
    const id = Homey.env.WEBHOOK_ID; // "56db7fb12dcf75604ea7977d"
    const secret = Homey.env.WEBHOOK_SECRET; // "2uhf83h83h4gg34..."
    const data = {
      // Provide unique properties for this Homey here
      $keys: ["aaa", "bbb"],
    };

    const myWebhook = await this.homey.cloud.createWebhook(id, secret, data);

    myWebhook.on('message', args => {
      this.log('Got a webhook message!');
      this.log('headers:', args.headers);
      this.log('query:', args.query);
      this.log('body:', args.body);
    });
  }
}

module.exports = App;

```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/app.mts" %}

```mts
import Homey from "homey";

type WebhookMessageArgs = {
  body: unknown;
  headers: Record<string, string>;
  query: Record<string, string>;
};

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    const id: string = Homey.env.WEBHOOK_ID; // "56db7fb12dcf75604ea7977d"
    const secret: string = Homey.env.WEBHOOK_SECRET; // "2uhf83h83h4gg34..."
    const data = {
      // Provide unique properties for this Homey here
      $keys: ["aaa", "bbb"],
    };

    const myWebhook = await this.homey.cloud.createWebhook(id, secret, data);

    myWebhook.on("message", (args: WebhookMessageArgs) => {
      this.log("Got a webhook message!");
      this.log("headers:", args.headers);
      this.log("query:", args.query);
      this.log("body:", args.body);
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/app.py" %}

```python
import homey
from homey import app
from homey.cloud_webhook import WebhookMessage


class App(app.App):
    async def on_init(self) -> None:
        id = homey.env["WEBHOOK_ID"]  # "56db7fb12dcf75604ea7977d"
        secret = homey.env["WEBHOOK_SECRET"]  # "2uhf83h83h4gg34..."
        data = {
            # Provide unique properties for this Homey here
            "$keys": ["aaa", "bbb"],
        }

        my_webhook = await self.homey.cloud.create_webhook(id, secret, data)

        def on_message(message: WebhookMessage) -> None:
            self.log("Got a webhook message!")
            self.log("headers:", message.get("headers"))
            self.log("query:", message.get("query"))
            self.log("body:", message.get("body"))

        my_webhook.on_message(on_message)


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}

The webhook HTTP request might look like this:

```http
POST /webhook/56db7fb12dcf75604ea7977d HTTP/1.1
X-Device-Id: aaa
Host: webhooks.athom.com
Content-Type: application/json; charset=utf-8

{
  "turned_on": true
}
```

Homey's webhook service will match your webhook based on `headers['X-Device-Id']` with value `aaa` (or `bbb`) and forward this to that Homey. If the webhook service received value `ccc` it will **not** be forwarded.

#### Example 2 — Body

{% tabs %}
{% tab title="JavaScript" %}
Using the same `data` object provided in `this.homey.cloud.createWebhook(id, secret, data)` as example 1, you can also define your *key path* in the body of your webhook HTTP request.
{% endtab %}

{% tab title="TypeScript" %}
Using the same `data` object provided in `this.homey.cloud.createWebhook(id, secret, data)` as example 1, you can also define your *key path* in the body of your webhook HTTP request.
{% endtab %}

{% tab title="Python" %}
Using the same `data` object provided in `self.homey.cloud.create_webhook(id, secret, data)` as example 1, you can also define your *key path* in the body of your webhook HTTP request.
{% endtab %}
{% endtabs %}

The webhook HTTP request might look like this:

```http
POST /webhook/56db7fb12dcf75604ea7977d HTTP/1.1
X-Device-Id: aaa
Host: webhooks.athom.com
Content-Type: application/json; charset=utf-8

{
  "X-Device-Id": "bbb",
  "turned_on": true
}
```

### Option 3 — Static webhook URLs using the Cloud Function (legacy)

{% hint style="danger" %}
Cloud Functions are now considered legacy and have been made "read-only", consider changing to Query Parameter or Key Path based webhooks.
{% endhint %}

If you had previously defined a custom *cloud function* on <https://tools.developer.homey.app/webhooks>, it is possible to view this function in the developer portal.

Your function has to match an incoming webhook against the `data` object provided in `this.homey.cloud.createWebhook(id, secret, data)`. For example an object with`{ deviceId: "aaabbbccc" }`.

The webhook sends the Device's ID in the webhook's body as `device_id`. Our cloud function therefore becomes:

```javascript
return homey_data.deviceId === webhook_data.device_id;
```

This function will execute for each Homey that is registered to this webhook. All Homeys that returned `true` will receive the webhook in the `webhook.on('message', ...)` listener.


# Guidelines

Before publishing your app your should read the Homey App Store guidelines to avoid getting your app submission rejected.

The Homey community is expanding and so are the number of app developers. In order to provide each user with a consistent experience, a set of guidelines has been designed for you, as an app developer, to follow. Whether you are a beginner or experienced developer, working solo or in a large team, implementing these guidelines will ensure a speedy review process.

## Before you submit your app

Ready to upload your app to the [Homey App Store](https://homey.app/apps)? We are very excited to see what you have created. We want to make sure that all Homey users have a great experience. Therefore, we will thoroughly review your app, before we can approve it.

Apps that are submitted for review for the first time need to be complete i.e. icons, images and required texts need to be present. Once we approve an app it can be released to the Homey App Store by the developer. To prevent apps in testing or prototyping stages from being released, we require that every app must meet our requirements before being published in the Homey App Store.

Before you submit your app for approval we advice to:

* Test your app for any crashes or bugs.
* Double check for any spelling errors.
* Check that your app follows the guidelines below.
* Provide Athom with the necessary devices to test your app *(for Verified Developers only)*.

## 1. Design

A coherent look and feel in both the Homey App Store and Homey's various user interfaces is essential to how users experience Homey. That is why the overall appearance for each submitted app is a key factor in our review process.

Each of the following articles will explain what is expected and what is definitely not desired.

### 1.1. App name

A clear app name is essential for new customers to find your app and understand what it's about. In most cases, the app's name should be exactly the same as the brand name.

1. In case your app supports a specific brand, use the brand name for your app. Company names are not allowed.
2. You may not use the trademarks Homey or Athom in your app's name.
3. You may not include protocol names (Zigbee, Z-Wave, 433 MHz, Infrared etc.) in your app's name.
4. Names that are longer than 4 words are not allowed. An app name should be short and simple, one that immediately clarifies what the app does.

| Do          | Don't                 |
| ----------- | --------------------- |
| Philips Hue | Lights by Philips Hue |
| tado°       | Tado Gmbh             |

### 1.2. Description

The description field is a required field to provide your app with a catchy tagline to grab the user's attention. The description is shown beneath your app's name and above the readme in the App Store. In case your app supports devices by a specific brand, consider using their slogan as your description.

1. Using your app's name or repeating text from the readme in your description is not allowed.
2. The description field is not meant for an extensive text. Provide an engaging one-liner that highlights the purpose of your app. In case your app supports devices by a specific brand, consider using their slogan as your description.
3. All apps in the Homey App Store add support for something to Homey, that is why they are there. So avoid descriptions such as these:
   * "Adds support for Sonos"
   * "Integrates Philips Hue with Homey"
   * "Control your Ikea devices with the Ikea app"

What we'd like to see:

* Philips Hue - "Transform the way you experience light"
* Ikea - "Create the right atmosphere for every mood"
* Rituals - "Your home never smelled smarter"
* Heimdall - "Turn Homey into a surveillance system"

### 1.3. Readme

The app's `readme.txt` must be used to provide an engaging summary of your app's purpose. Describe why your app's integration is useful in day-to-day life.

1. Keep the text short and concise, one to two paragraphs tops. The text should be pleasant to read, so stick to single line spacing and avoid unnecessary indentations. Headers or titles are not needed.
2. In order to keep your readme short, do not list all the different features, capabilities and Flow cards available in your app. Give credit to contributors in your App Manifest instead of your readme.
3. The readme text is displayed in plain text, any Markdown format in your readme is not allowed and will not be rendered.
4. URLs in the readme are not allowed. See section [URLs](#urls) for more information.
5. Don't create a changelog in your readme. In case you are updating your app, use the Changelog functionality of the Homey App Store. When publishing your app, Homey will ask your 'What's new?" and a `.homeychangelog.json` file is automatically created in the root directory of your app. Describe your changes clearly, so your users know what has changed.

### 1.4. Images

Images are a great way to uplift your app's experience. Both the app and driver images are prominently visible on the Homey App Store page, so make sure they are visually compelling. Use brand images if this is possible. If you want to create custom images, keep it clean, simple and do not use a Homey or Homey logo in the images. Make sure the image is clear, well designed and recognizable for the respective brand or purpose.

**1.4.1. Format & resolutions**

All images must be provided in either `.jpg` or `.png` format. Each chosen image should be included in two resolutions. Small and large are mandatory, but you can add a third: XLarge for better resolution on large or high-resolution screens.

Resolutions for the app image:

* Small 250 x 175
* Large 500 x 350
* XLarge 1000 x 700

Resolutions for the driver images:

* Small 75 x 75
* Large 500 x 500
* XLarge 1000 x 1000

{% file src="/files/-MdWWnD6brS4Z3iRnbru" %}
Homey App Store template for Sketch
{% endfile %}

**1.4.2. App images**

The app image should be a lively, visually appealing image that represents the purpose of your app. Lifestyle images and brand images are great examples and are strongly encouraged.

Images that consist of a single flat shape or icon on a plain, monochrome or transparent background are not approved. For example, a black shape on a white background will look flat and unappealing in the Homey App Store, rather than inviting.

Avoid a logo as app image:

![](/files/-MYid6ftAVLOeq5eG4UW)

Avoid clipart or icon type of images:

![](/files/-MYid6fuC32xL_knJMv3)

Avoid images that solely contain Android or iOS app examples:

![](/files/-Me6XVVS--FkcaBvGlC0)

**1.4.3. Driver images**

In case your app has drivers, make sure to provide individual images for each driver. A driver image should have a white background and a recognizable picture of the device it supports.

Don't use the app image as a driver image.

![](/files/-MYid6fvYZdPC09jXszN)

Don't use your app icon as a driver image.

![](/files/-MYid6fwzENgwiCs9BiS)

{% hint style="warning" %}
Do not use the Homey logo, name or device in any of your images.
{% endhint %}

### 1.5. App Icons

The app icon is one of the first things users will see when they search for your app in the Homey App Store. It is therefore important that it is immediately recognisable and representative of your app or brand.

The app icon is a clean, vector-based drawing that should accurately represent the brand or purpose of your app. It should be recognisable at small sizes, and have a transparent background. So do not use images, filled illustrations, gradients, or background colours in your app icon. Submitting a filled image or illustration as an icon will cause it to appear as a solid shape, which is not recognisable at small sizes and will not be approved.

Key points:

* If your app supports a specific brand, use the company's brand icon.
* Icons must have a transparent background.
* Always use the full canvas (960x960px) so the icon is displayed properly.

Don't use a driver icon as your app icon:

![](/files/-MYoprnKCcjhyVNdFzeE)

Don't use a background color in your icon:

![](/files/-MYid6fyLHdb0r0XUPgo)

### **1.6. Driver icons**

Driver icons are visible once an app has been installed and the user adds a device to Homey. Each individual driver your app supports must have its own unique icon that clearly represents the device.

An icon is a clean, vector-based drawing that accurately represents the device it belongs to. It should be recognisable at small sizes, so use the full canvas, and have a transparent background. Icons are drawn using lines and shapes to depict the form of the device, with dimension added through the use of angles and line work.

Avoid using images, filled illustrations, gradients, or background colours. Submitting a filled image or illustration as an icon will cause it to appear as a solid shape, which is not recognisable and will not be approved.

{% hint style="info" %}
If you do not have suitable icons for your app or drivers we can provide a handmade icon for you. You can request an icon by creating an issue in the [Homey Vectors](https://github.com/athombv/homey-vectors-public) repository. Please include as much information about your app as possible and include pictures of the devices.
{% endhint %}

Want to try your hand at creating your own driver icons? Here's a few things to keep in mind:

1. The canvas size should be 960x960px.
2. Always use the full canvas, so the icon is displayed properly.
3. Where possible, use an angle from the right side rather than a front facing view, to add dimension to the icon.
4. Icons must have a transparant background.
5. Do not re-use the app icon for your drivers.
6. Make sure to use the correct line width.

<div align="left"><figure><img src="/files/DbctiCrMOInWTOrl2oUk" alt="" width="375"><figcaption></figcaption></figure></div>

Add dimension to your icons and use a right side angle:

<figure><img src="/files/slChzHM5nf7tCceWH0Ed" alt=""><figcaption></figcaption></figure>

Don't use driver images as an icon:

![](/files/-MYid6fzPS-ts9d1UoDD)

Don't re-use the app's icon for your drivers:

![](/files/-MYid6g-mbk3z13ijs9N)

Don't use a background color, icons should have a transparent background:

![](/files/LrMyrNBBbVdCUWdyYMEh)

### 1.7. Brand color

The property brandColor is a mandatory value which needs to be added to your App Manifest. The defined color is used as a backdrop for your icons in e.g. Flows, Add devices and the App Store.

Both your app icon and driver icons must be clearly visible against the background color. Use brand colors to complement the icons and make them more recognizable.

![](/files/-MYiLNfRJJcrSzdAPjN3)

### 1.8. URLs

Offer extra help or information to your users, simply by adding a URL to your App Manifest. Each URL will be visible as a clickable link, at the lower section of your app page.

![](/files/-MXlRcQ5mi8s6bKJlXTW)

Read the App Manifest guide for more information.

{% content-ref url="/pages/-MWdB31Roc8MjJuI0j5f" %}
[Manifest](/the-basics/app/manifest)
{% endcontent-ref %}

### 1.9. Flow

Flow is an essential part of Homey. Incorporate When-And-Then Flow cards, so that users can integrate your app in their automated home environment.

**1.9.1. Titles**

Flow card titles need to be short and clear, so the user instantly knows what the trigger, condition or action does. Device names should not be mentioned in the title. Don't add the When, And, Then statements to the title. Do not use parantheses in Flow titles.

|      | Do                       | Don't                                      |
| ---- | ------------------------ | ------------------------------------------ |
| When | Unknown face is detected | Netatmo Presence detected an unknown face. |
| And  | Is !{{on\|off}}          | And the light is off                       |
| Then | Lock door                | Going to lock the door                     |

**1.9.2. Formatted titles**

Flow cards may contain arguments. To integrate an argument into the title of the Flow card the `titleFormated` property must be used. Make sure that the title is still straightforward and clear with the arguments incorporated.

```javascript
  "title": { "en": "Run a script" },
  "titleFormatted": { "en": "Run [[Script]]" }
```

**Example**: A remote or wall switch can have multiple buttons, with several actions to trigger them. For example pressed once, twice or a long press. Instead of creating several Flow cards for all these triggers, one flow card can be created containing several arguments.

```javascript
  "title": { "en": "Button was pressed" },
  "titleFormatted": { "en": "[[buttontype]] button was pressed [[scene]]" }
```

**1.9.3. Hint**

In case the function of the Flow card is not obvious use the hint property to give additional information.

```javascript
  "title": { "en": "Battery state changed" },
  "hint": { "en": "This card starts a Flow when a change in the battery state is observed." }
```

### 1.10. Widget Previews

The Widget Preview is a simplified representation of your Widget. If your app includes a Widget, the preview will be displayed on the App Store page of your app as well as in the Widget picker within the Homey Mobile app. The preview should give users an idea of the Widget’s appearance without revealing too much detail. Both light and dark mode versions should be provided.

We highly recommend using our [Figma Template](https://www.figma.com/community/file/1392859749687789493/widget-previews-template) for creating these previews. The template includes the proper color styles and shadows and will automatically generate a dark mode version. It also provides examples and ensures export in the correct dimensions (1024x1024). In addition to using the template, please keep the following guidelines in mind.

**Don't use screenshots or provide over detailed designs:**

* Don’t include text.
* Use simple shapes.

<figure><img src="/files/AnqZYLlHpbWoSOMO9AJB" alt=""><figcaption></figcaption></figure>

**Use the color styles provided in the Figma template for basic elements:**

* Don't use the same colors as the Widget picker background.
* Try not to use too many different colors.
* Use the shadow styles provided in the template.

<figure><img src="/files/Gwozs0LTXoVyxlOj0mho" alt=""><figcaption></figcaption></figure>

**Don't use a background color or image:**

* Previews should have a transparent background.

<figure><img src="/files/ECernCoh5xuiy1STdfp3" alt=""><figcaption></figcaption></figure>

### 1.11. Language & Translations

English is the required language for your app, however additional languages are also allowed and encouraged. For more details on implementing translations read the [internationalization guide](/the-basics/app/internationalization).

1. Make sure your app doesn't have any typos, language or spelling errors. Your app will be rejected if spelling errors are found.
2. Consistency in translations is vital. A partially translated app is very confusing for users. So avoid sporadic translations throughout the app. If the description is translated to a certain language, the readme should be translated to that language as well. In case you translate Flow cards, make sure to translate all Flow cards, device settings, capabilities, etc.

### 1.12. Dependencies

Apps may communicate to other apps using App-to-App communication to enhance an app's functionality. It is however not permitted to make one app fully dependent on another app. An app's core functionality must always work standalone.

It is also not allowed to publish an app which does not add any value by itself, but is meant to be used by other apps. In such scenarios, embed such an app's functionality directly.

### 1.13. Account

Your app will be published with your Athom account. A developer account name cannot contain emojis, special characters or inappropriate language. Account names can be adjusted via [accounts.athom.com](https://accounts.athom.com)

If your account has the [verified developer badge](/guides/homey-cloud#verified-apps), the account name should be the name of the company publishing the app. A few examples:

{% hint style="success" %}
*Bosch-Siemens Home Connect* published by "**Home Connect GmbH**"\
\&#xNAN;*Frient* published by "**frient A/S**"\
\&#xNAN;*Yale Access* published by "**Yale Access**"\
\&#xNAN;*Plugwise* published by "**Plugwise B.V.**"
{% endhint %}

## 2. Legal

### 2.1. Duplicate

**2.1.1. App**

As a community developer, you are part of a community that is working towards a common goal of making home automation accessible to everyone. This means we must keep things simple. Ideally there is only one app per brand or concept in the App Store, because too many apps with the same purpose will confuse users. So make sure your app is one of a kind - just like you.

Before submitting your app, please check the Homey App Store to see if a similar app already exists. If it does, we encourage you to reach out to the existing developer first and explore the possibility of working together, for example by contributing via a Pull Request. Collaboration benefits everyone, it keeps the App Store clean and ensures users have access to the best possible integration.

If collaboration is not possible, you are still welcome to submit your app. However, please make sure to clarify in your submission why collaboration was not possible, and why a separate app is necessary. Submissions that resemble an existing app without any clarification will be rejected.

In case an existing app is no longer maintained and a new alternative has been submitted, we may reach out to the original developer to discuss transferring the app or removing it from the App Store.

**2.1.2. Code**

There is a lively and open Homey community, with many developers that keep their source code open for others to view, or even contribute. Copying or using code created by fellow developers without consent is an infringement on their intellectual property.

If your code is based on source code that is not your own, always ask for permission and give credit to the original source in the app manifest. If at any point it is revealed that code has been used without consent your app will be at risk of being removed from the Homey App Store.

### 2.2. Explicit content

Apps with adult content (e.g. pornography) are not allowed.

### **2.3. Compensation**

Apps that require payment for partial or all functionality are not allowed. Apps should be free of charge for all users. We encourage you to add [donation options](https://apps.developer.homey.app/the-basics/app/manifest#contributing) to your app.

Apps whose goal is to connect to a service that requires payment (for example a Premium-tier for a smart thermostat), and the payment happens on the integration product's end, this app can still be offered for free in the Homey App Store.

## 3. After app submission

Once an app has been submitted for certification, the app will be reviewed according to the various criteria mentioned in these guidelines. Athom holds the right to make the final decision if an app will be approved or removed from the Homey App Store.

### 3.1. Review duration

After your app has been submitted, your submission will be reviewed. This process can take up to 2 weeks. Within this time an app can either be approved, receive feedback or we might inquire more information before proceeding with the review. If an app meets all the requirements upon submission, the review process can go much faster.

The review process for a new app created by a Verified Developer can take longer, depending on the size of the app and all its capabilities.

### 3.2. Testing your app (Verified Developers only)

It is imperative that an app created by a Verified Developer is of the highest quality and delivers a great user experience. To assure that these standards are met, an app created by a Verified Developer will be thoroughly tested before its approval.

In order for the app to be tested by our testing team, make sure to provide a few sample devices prior of your submission. For cloud apps a demo account can be an option as well.\
If devices are to large or not available consult with us to determine how we can proceed with the review.

To ensure a smooth approval process, we expect an app has been thoroughly tested **before** it is submitted for certification.\
Once the app has been published to **Test** in the [Developer Tools](https://tools.developer.homey.app/apps) a testing URL will be available which can be shared with beta testers.

{% hint style="info" %}
[https://homey.app/en-us/app/APP.ID/test/](https://homey.app/en-us/app/appid/test) replace with your own app ID
{% endhint %}

Make sure to test the following:

* [ ] Use the device controls in Homey to update the device state.
* [ ] Update the device advanced settings in Homey (if available).
* [ ] Create Flows with custom Flow cards (if available) and trigger/execute the Flow.
* [ ] Manually adjust device state outside of the Homey app, and verify that state are updated in Homey. For example when manually setting a temperature on your thermostat, the set temperature should be updated in Homey.

An app will be tested not only on correct functionality, but also the user experience and usability are of high importance. So make sure that:

* [ ] The pairing instructions for all drivers are accurate and clear.
* [ ] Error messages are understandable.
* [ ] Flow cards titles are intuitive and understandable.
* [ ] Label and hint texts for driver advanced settings are clear and understandable.
* [ ] Every aspect of the app has been accurately translated.

Apps with a history of high quality submissions will often experience a faster review process.

### 3.3. Feedback

Any findings or feedback during the review process will be shared with the developer and is expected to be implemented.

### 3.4. Removal

Apps may be hidden or removed from the Homey App Store if they are no longer compatible with Homey, functioning correctly, or actively maintained.

Apps that have not received an update in two years or more may be considered abandoned. However, an app will not automatically be marked as abandoned solely based on its last update date. If the app is still functioning correctly, it will remain available in the Homey App Store.

An app may be hidden or removed if one or more of the following applies:

* The app has not been updated in two years or more and/or is no longer functioning correctly
* The app has not been updated in two years or more and a working alternative has been submitted by another developer

If your app has been marked as abandoned we will let you know. If you are no longer able to maintain your app, consider transferring it to another developer.


# Publishing

Publishing your app in the Homey App Store allows you to share it with other Homey users.

## How to publish?

![](/files/-MYtTz5Rm2JQHDrPqG4w)

Publishing is as easy as running the following command from your app directory:

```bash
homey app publish
```

{% hint style="info" %}
Before you publish your app make sure to read the [App Store Guidelines](/app-store/guidelines). This will ensure the app review process goes smoothly and your app won't be rejected.
{% endhint %}

Your app will be compressed and send to the Homey App Store for processing. Go to <https://tools.developer.homey.app>, 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 App Store and becomes available to all Homey users.

{% hint style="info" %}
Apps that have never been released to the Homey App Store, will need to be certified before becoming publicly available for other users. In case you want to publish a *Test* version of the app make sure to disable the "publish directly after approval" checkbox when submitting for certification.
{% endhint %}

## Requirements

To check if all requirements are met, validate your app prior to submission.

```bash
homey app validate --level publish
```

For Verified Homey Apps there are some additional requirements which can be validated with the `verified` validation level. This level will be applied by default if you are logged in with a verified developer account.

```bash
homey app validate --level verified
```

| Validation level | Description                                                                                                                                                                                                                                                |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `debug`          | The debug validation level is used during development. Various app manifest properties, such as `images`,`brandColor`, and`category` are optional at this validation level.                                                                                |
| `publish`        | You app needs to pass this validation level to be published to the Homey App store for Homey Pro.                                                                                                                                                          |
| `verified`       | If you are a verified app developer your app needs to pass this validation level. This is required for Homey Cloud. The verified validation level adds additional requirements such as adding `platforms`, `connectivity`, and `support` to your manifest. |

## Visibility

The Homey App Store overview page shows various categories, in which apps are sorted. Only the best, most popular and visually appealing apps are shown here.

To get your app displayed on the Homey App Store overview page, make sure that your app not only works great but also looks amazing. So add a catchy Description, beautiful App Image, great Icon. Capture the attention of your users!

To get an idea of which type of apps are displayed check out the [App Store overview](https://homey.app/nl-nl/apps/homey-pro/) page.

## Automating within GitHub Actions

To automate the validation, versioning and publishing of your Homey app within GitHub Actions, you can use the following Actions from the GitHub Marketplace.

* <https://github.com/marketplace/actions/homey-app-validate>
* <https://github.com/marketplace/actions/homey-app-update-version>
* <https://github.com/marketplace/actions/homey-app-publish>


# Verified Developer

A verified developer can be recognised by the blue 'Official' badge behind the developer name the [Homey App Store](https://homey.app/en-nl/apps/homey-pro/).

<figure><img src="/files/iey6eyKMKlJNUKrWebxd" alt="" width="160"><figcaption></figcaption></figure>

A verified developer badge signals to the user that the app was created by or on behalf of the brand itself or by Athom. It also allows the developer to publish their app for [Homey Cloud](/guides/homey-cloud).

### Who can become a verified developer?

Organizations that are a registered business or institution can apply for the verified developer program.

Individuals are not eligible to become a verified developer at this time.

### How to become a verified developer?

In the [Homey Developer Tools](https://tools.developer.homey.app/), navigate to **My Organizations** and select your organization. If you don't have an organization yet, you can create one on that page.

Click the **Request Verification** button to send an e-mail, and be sure to include as many details as necessary for us to evaluate your verified developer request.

Additionally, we can look at the possibilities of your brand becoming an official [Works with Homey Partner](https://homey.app/en-us/wiki/works-with-homey-program/). Becoming an official partner opens up the door to a world of possibilities, like participating in cross-marketing activities.


# Updating

After fixing bugs or adding new features you can to release an update to the Homey App Store. Read this article for useful tips and considerations.

## Writing a changelog

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.

## App versions

The version of your app should be indicated using [semver](https://semver.org). This version indication is used to determine whether an app should be updated for users. If there is a higher version available for a user, it will be automatically installed. Therefore, make sure that the app you are submitting has a version that is *higher* than the current *Live* version.

{% hint style="warning" %}
Homey will never downgrade apps. If you want to undo a release, then re-submit an older build with a higher version number.
{% endhint %}

## Update your Live app while having a newer version in Test

Sometimes it may be needed to patch the *Live* version of your app while also having a *Test* version of your app published. This is possible by submitting a patch with a version *lower* than your current *Test* version. When an app update with a version lower than the current *Test* version is created (e.g. v2.0.1), it will replace the current *Test* version (e.g. v3.0.0). Therefore, if you have published your patch to *Live* and you want to make your previous *Test* version available again, you must release the *Test* version with a higher version number than the highest version ever released (e.g. v3.0.1).

The users of the *Live* version will automaticallly update to the new patch version (e.g. v2.0.1), and the users of the *Test* version will automatically update to the new *Test* version (e.g. v3.0.1). If you do not upload a new *Test* version, users using this *Test* version (e.g. v3.0.0) will not automatically downgrade, but stay on their currently installed version (e.g. v3.0.0).

* Let's say you have version 1.0.0 as *Live* version and version 2.0.0 as *Test* version. Version 1.0.0 contains a critical bug that must be fixed immediately.
* First you submit a fix for the bug in version 1.0.1.
* Version 1.0.1 will become the *Test* version, which you can test. The previous *Test* version 2.0.0 will become unavailable, but existing users of 2.0.0 can keep using it.
* If the bug is fixed, you can set version 1.0.1 to *Live*. Users of version 1.0.0 will automatically update to this version. Existing users of 2.0.0 will not downgrade.
* Now the previous version 2.0.0 can be set back to *Test*, but since the version 2.0.0 is already taken by a previous build it must be resubmitted as version 2.0.1.


# Custom Views

Custom views allow you to create interfaces specific to your app.

Homey Apps can have custom views, these are regular web pages (HTML/CSS/JS) that will be shown to the users of your app. These pages will have access to a global `Homey` api that they can use to communicate with Homey and your app.

There are three types of custom views available for Homey Apps:

{% content-ref url="/pages/-MWORiDwURDrP3t0v7ay" %}
[App Settings](/advanced/custom-views/app-settings)
{% endcontent-ref %}

{% content-ref url="/pages/-MYtLHyrt9YP8RPcKPCu" %}
[Custom Pairing Views](/advanced/custom-views/custom-pairing-views)
{% endcontent-ref %}

{% content-ref url="/pages/XyArR9HsPghISLXQ4wdP" %}
[Widgets](/the-basics/widgets)
{% endcontent-ref %}

### Style library

{% content-ref url="/pages/jcx6OKtRnkMWjOp0UaEH" %}
[HTML & CSS Styling](/advanced/custom-views/html-and-css-styling)
{% endcontent-ref %}


# App Settings

App Settings allow you to add custom HTML pages, that users can view to modify your app's settings. App Settings can be used for global configuration parameters for your app.

{% tabs %}
{% tab title="JavaScript" %}
A Homey App can save settings that are persistent across reboots. These settings can be accessed from anywhere in your app through [`ManagerSettings`](https://apps-sdk-v3.developer.homey.app/ManagerSettings.html). You can also create a page where users can update the App Settings.

{% hint style="info" %}
Custom app setting views are not allowed on Homey Cloud. Any information your app might need to function should be asked for when pairing a device. Read the [Custom Pairing Views](/advanced/custom-views/custom-pairing-views) documentation for more information.
{% endhint %}

{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  async onInit() {
    const username = this.homey.settings.get('username');
    // ...
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
A Homey App can save settings that are persistent across reboots. These settings can be accessed from anywhere in your app through [`ManagerSettings`](https://apps-sdk-v3.developer.homey.app/ManagerSettings.html). You can also create a page where users can update the App Settings.

{% hint style="info" %}
Custom app setting views are not allowed on Homey Cloud. Any information your app might need to function should be asked for when pairing a device. Read the [Custom Pairing Views](/advanced/custom-views/custom-pairing-views) documentation for more information.
{% endhint %}

{% code title="/app.mts" %}

```mts
import Homey from "homey";

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    const username = this.homey.settings.get("username");
    // ...
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
A Homey App can save settings that are persistent across reboots. These settings can be accessed from anywhere in your app through [`ManagerSettings`](https://python-apps-sdk-v3.developer.homey.app/manager/settings.html#homey.manager.settings.ManagerSettings). You can also create a page where users can update the App Settings.

{% hint style="info" %}
Custom app setting views are not allowed on Homey Cloud. Any information your app might need to function should be asked for when pairing a device. Read the [Custom Pairing Views](/advanced/custom-views/custom-pairing-views) documentation for more information.
{% endhint %}

{% code title="/app.py" %}

```python
from homey import app


class App(app.App):
    async def on_init(self) -> None:
        username = self.homey.settings.get("username")
        ...


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}

## Creating an App Settings page

To create a settings view, create a folder named `/settings/` in your app's root, and create a new file named `index.html` in it.

Include the following script in your `<head>`:

{% code title="/settings/index.html" %}

```markup
<head>
  <!-- ... -->
  <script
    type="text/javascript"
    src="/homey.js"
    data-origin="settings"
  ></script>
</head>
```

{% endcode %}

Next, add a function called `onHomeyReady`:

{% code title="/settings/index.html" %}

```markup
<script type="text/javascript">
  function onHomeyReady(Homey) {
    // ...

    Homey.ready();
  }
</script>
```

{% endcode %}

The first argument of `onHomeyReady` will be a `Homey` instance, which can be used to communicate with Homey.

Finally, call `Homey.ready()` to show the settings view.

### Example

Below is a simple example to save a username & password. Learn more about[ HTML usage and CSS styling.](/advanced/custom-views/html-and-css-styling)

<figure><img src="/files/E38cx25doNyVQZuHtzG8" alt=""><figcaption></figcaption></figure>

<pre class="language-markup" data-title="/settings/index.html"><code class="lang-markup"><strong>&#x3C;!DOCTYPE html>
</strong>&#x3C;html>
  &#x3C;head>
    &#x3C;!-- The '/homey.js' script must be included in your settings view to work -->
    &#x3C;script
      type="text/javascript"
      src="/homey.js"
      data-origin="settings"
    >&#x3C;/script>
  &#x3C;/head>
  &#x3C;body>
    &#x3C;header class="homey-header">
      &#x3C;h1 class="homey-title" data-i18n="settings.title">
        &#x3C;!-- This will be filled with the translated string with key 'settings.title'. -->
      &#x3C;/h1>
      &#x3C;p class="homey-subtitle" data-i18n="settings.subtitle">
        &#x3C;!-- This field will also be translated -->
      &#x3C;/p>
    &#x3C;/header>

    &#x3C;fieldset class="homey-form-fieldset">
      &#x3C;legend class="homey-form-legend">My Settings&#x3C;/legend>

      &#x3C;div class="homey-form-group">
        &#x3C;label class="homey-form-label" for="username">Username&#x3C;/label>
        &#x3C;input class="homey-form-input" id="username" type="text" value="" />
      &#x3C;/div>
      &#x3C;div class="homey-form-group">
        &#x3C;label class="homey-form-label" for="password">Password&#x3C;/label>
        &#x3C;input class="homey-form-input" id="password" type="password" value="" />
      &#x3C;/div>
    &#x3C;/fieldset>

    &#x3C;button id="save" class="homey-button-primary-full">Save changes&#x3C;/button>

    &#x3C;script type="text/javascript">
      // a method named 'onHomeyReady' must be present in your code
      function onHomeyReady(Homey) {
        // Tell Homey we're ready to be displayed
        Homey.ready();

        var usernameElement = document.getElementById("username");
        var passwordElement = document.getElementById("password");
        var saveElement = document.getElementById("save");

        Homey.get("username", function (err, username) {
          if (err) return Homey.alert(err);
          usernameElement.value = username;
        });

        Homey.get("password", function (err, password) {
          if (err) return Homey.alert(err);
          passwordElement.value = password;
        });

        saveElement.addEventListener("click", function (e) {
          Homey.set("username", usernameElement.value, function (err) {
            if (err) return Homey.alert(err);
          });
          Homey.set("password", passwordElement.value, function (err) {
            if (err) return Homey.alert(err);
          });
        });
      }
    &#x3C;/script>
  &#x3C;/body>
&#x3C;/html>
</code></pre>

{% code title="/locales/en.json" %}

```javascript
{
  "settings": {
    "title": "My Settings Page",
    "subtitle": "Please log in"
  }
}
```

{% endcode %}

## Settings View API

### `Homey.ready()`

The settings view will be hidden until this method has been called. Use the extra time to make required API calls to prevent flickering on screen.

### `Homey.get( [String name,] Function callback )`

Gets a single setting's value when `name` is provided, or an object with all settings when `name` is omitted.

### `Homey.set( String name, Mixed value, Function callback )`

Sets a single setting's value. The value must be JSON-serializable.

### `Homey.unset( String name, Function callback )`

Unsets a single setting's value.

### `Homey.on( String event, Function callback )`

Register an event listener for your app's realtime events. System events when modifying settings are: `settings.set`, `settings.unset`.

### `Homey.api( String method, String path, Mixed body, Function callback )`

Make a call to your App's Web API. The first argument `method` can be either `GET`, `POST`, `PUT` or `DELETE`. The second argument `path` is relative to your app's API endpoint. The third argument `body` is optional, and `null` can be provided to ignore it. For example:

{% code title="/settings/index.html" %}

```markup
<script type="text/javascript">
  // make a PUT call to /api/app/com.your.app/hello
  Homey.api("PUT", "/hello", { foo: "bar" }, function (err, result) {
    if (err) return Homey.alert(err);
  });
</script>
```

{% endcode %}

### `Homey.alert( String message, Function callback )`

Show an alert dialog.

### `Homey.confirm( String message, Function callback )`

Show a confirm dialog. The callback's 2nd argument will be `true` when the user pressed `OK`.

### `Homey.popup( String url[, Object opts] )`

Show a popup (new window). The object `opts` can optionally have a `width` and `height` property of type `number`. The default width and height is `400`.

### `Homey.openURL( String url )`

Show a new window.

### `Homey.__( String key [, Object tokens] )`

Translate a string programmatically. The first argument `key` is the name in your `/locales/__language__.json`. Use dots to get a sub-property, e.g. `settings.title`. The optional second argument `tokens` is an object with replacers. Read more about translations in the [internationalization guide](/the-basics/app/internationalization).


# Custom Pairing Views

Most drivers will suffice using the provided pairing templates. Some advanced drivers however can benefit from creating their own views.

The pairing views consists of `.html` files in the `/drivers/<driver_id>/pair`-folder. Where the name of the file is the view ID, as `id` described in the App Manifest.

The pairing views have a few Homey-specific JavaScript functions available. They are documented below.

## Back-end API

The `session` property passed in `onPair` can control the front-end programmatically.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/drivers/\<driver\_id>/driver.js" %}

```javascript
const Homey = require('homey');

class Driver extends Homey.Driver {
  async onPair(session) {
    // Show a specific view by ID
    await session.showView("my_view");

    // Show the next view
    await session.nextView();

    // Show the previous view
    await session.prevView();

    // Close the pair session
    await session.done();

    // Received when a view has changed
    session.setHandler("showView", async function (viewId) {
      console.log("View: " + viewId);
    });
  }
}

module.exports = Driver;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/drivers/\<driver\_id>/driver.mts" %}

```mts
import Homey from "homey";

export default class Driver extends Homey.Driver {
  async onPair(session: Homey.Driver.PairSession): Promise<void> {
    // Show a specific view by ID
    await session.showView("my_view");

    // Show the next view
    await session.nextView();

    // Show the previous view
    await session.prevView();

    // Close the pair session
    await session.done();

    // Received when a view has changed
    session.setHandler("showView", async (viewId: string) => {
      this.log("View:", viewId);
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/drivers/\<driver\_id>/driver.py" %}

```python
from homey import driver
from homey.pair_session import PairSession


class Driver(driver.Driver):
    async def on_pair(self, session: PairSession) -> None:
        # Show a specific view by ID
        await session.show_view("my_view")

        # Show the next view
        await session.next_view()

        # Show the previous view
        await session.prev_view()

        # Close the pair session
        await session.done()

        # Received when a view has changed
        async def on_show_view(view_id: str) -> None:
            self.log("View:", view_id)

        session.set_handler("showView", on_show_view)


homey_export = Driver

```

{% endcode %}
{% endtab %}
{% endtabs %}

## Front-end API

The following methods are available at the front-end to communicate with the back-end. The `Homey` object is globally available.

### Emit an event

`Homey.emit( String event, Mixed data ): Promise<any>`

Emit an event to your app. The function called will be the one registered by `session.setHandler( String event, Function callback )` in your driver implementation.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "class": "socket",
  "capabilities": ["onoff", "dim"],
  "images": {
    "small": "/drivers/my_driver/assets/images/small.png",
    "large": "/drivers/my_driver/assets/images/large.png",
    "xlarge": "/drivers/my_driver/assets/images/xlarge.png"
  },
  "pair": [
    {
      "id": "my_view"
    }
  ]
}
```

{% endcode %}

{% code title="/drivers/\<driver\_id>/pair/my\_view\.html" %}

```markup
<script type="application/javascript">
  Homey.emit("my_event", { foo: "bar" }).then(function (result) {
    console.log(result); // result is: Hello!
  });
</script>
```

{% endcode %}

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/drivers/\<driver\_id>/driver.js" %}

```javascript
const Homey = require('homey');

class Driver extends Homey.Driver {
  async onPair(session) {
    session.setHandler("my_event", async function (data) {
      // data is { 'foo': 'bar' }
      return "Hello!";
    });
  }
}

module.exports = Driver;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/drivers/\<driver\_id>/driver.mts" %}

```mts
import Homey from "homey";

type MyEventData = {
  foo: string;
};

export default class Driver extends Homey.Driver {
  async onPair(session: Homey.Driver.PairSession): Promise<void> {
    session.setHandler("my_event", async (data: MyEventData): Promise<string> => {
      return "Hello!";
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/drivers/\<driver\_id>/driver.py" %}

```python
from typing import TypedDict

from homey import driver
from homey.pair_session import PairSession


class MyEventData(TypedDict):
    foo: str


class Driver(driver.Driver):
    async def on_pair(self, session: PairSession) -> None:
        async def on_my_event(data: MyEventData) -> str:
            return "Hello!"

        session.set_handler("my_event", on_my_event)


homey_export = Driver

```

{% endcode %}
{% endtab %}
{% endtabs %}

### Receive an event

`Homey.on( String event, Function callback )`

Listen to a message from your app. You can trigger this function from your app by calling `session.emit()`.

{% code title="/drivers/\<driver\_id>/pair/start.html" %}

```markup
<script type="application/javascript">
  Homey.on("hello", function (message) {
    Homey.alert(message); // Hello to you!
    return "Hi!"; // send a reply back to the pairing session

    // you can return a promise if you need to do some async
    // work before replying to the message.
  });
</script>
```

{% endcode %}

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/drivers/\<driver\_id>/driver.js" %}

```javascript
const Homey = require('homey');

class Driver extends Homey.Driver {
  async onPair(session) {
    session.setHandler("showView", async (viewId) => {
      if (viewId === "start") {
        const data = await session.emit("hello", "Hello to you!");
        console.log(data); // Hi!
      }
    });
  }
}

module.exports = Driver;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/drivers/\<driver\_id>/driver.mts" %}

```mts
import Homey from "homey";

export default class Driver extends Homey.Driver {
  async onPair(session: Homey.Driver.PairSession): Promise<void> {
    session.setHandler("showView", async viewId => {
      if (viewId === "start") {
        const data: string = await session.emit("hello", "Hello to you!");
        console.log(data); // Hi!
      }
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/drivers/\<driver\_id>/driver.py" %}

```python
from typing import TypedDict

from homey import driver
from homey.pair_session import PairSession


class MyEventData(TypedDict):
    foo: str


class Driver(driver.Driver):
    async def on_pair(self, session: PairSession) -> None:
        async def on_show_view(view: str) -> None:
            if view == "start":
                data: str = await session.emit("hello", "Hello to you!")
                print(data)  # Hi!

        session.set_handler("showView", on_show_view)


homey_export = Driver

```

{% endcode %}
{% endtab %}
{% endtabs %}

### Set a Title

`Homey.setTitle( String title )`

Set the window's title.

#### Example

{% code title="/drivers/\<driver\_id>/pair/start.html" %}

```markup
<script type="application/javascript">
  Homey.setTitle(Homey.__("pair.title"));
</script>
```

{% endcode %}

### Set a Subtitle

`Homey.setSubtitle( String subtitle )`

Set the window's subtitle.

#### Example

{% code title="/drivers/\<driver\_id>/pair/start.html" %}

```markup
<script type="application/javascript">
  Homey.setSubtitle(Homey.__("pair.subtitle"));
</script>
```

{% endcode %}

###

### Show a View

`Homey.showView( String viewId )`

Navigate to another view. The parameter `viewId` should be an ID as specified in your App Manifest.

{% code title="/drivers/\<driver\_id>/pair/start.html" %}

```markup
<script type="application/javascript">
  Homey.showView("list_devices");
</script>
```

{% endcode %}

### Previous View

`Homey.prevView()`

Show the previous view.

### Next View

`Homey.nextView()`

Show the next view.

### Get the current view

`Homey.getCurrentView()`

Returns the current view ID.

### Create a device

`Homey.createDevice( Object device ): Promise<Object>`

Create a device with the properties in `device`.

The `device` object must at least contain the properties `data` and `name` and may contain `icon`, `class`, `capabilities`, `capabilitiesOptions`, `store` and `settings`.

#### Example:

{% code title="/drivers/\<driver\_id>/pair/start.html" %}

```markup
<script type="application/javascript">
  Homey.createDevice({
    // The name of the device that will be shown to the user
    name: "My Device",

    // The data object is required and should contain only unique properties for the device.
    // So a MAC address is good, but an IP address is bad (can change over time)
    data: {
      id: "abcd",
    },

    // Optional: The store is dynamic and persistent storage for your device
    store: {
      // For example store the IP address of your device
      address: "127.0.0.1",
    },

    // Optional: Initial device settings that can be changed by the user afterwards
    settings: {
      pincode: "1234",
    },
  })
    .then(function (result) {
      Homey.done();
    })
    .catch(function (error) {
      Homey.alert(error);
    });
</script>
```

{% endcode %}

### Get current zone

`Homey.getZone(): Promise<string>`

Get the Zone ID of the active Zone. The promise resolves to the zone id.

### Get view options

`Homey.getOptions( [String viewId] ): Promise<Object>`

Get the options of a view, or the current view when `viewId` is omitted. The promise resolves to the `viewOptions` of the specified view.

View options may be added to a view by specifying an `options` object in the App Manifest.

### Set navigation close

`Homey.setNavigationClose()`

Remove all navigation buttons and show a single *Close* button.

### Close the pair session

`Homey.done()`

Close the pairing window.

### Alert dialog

`Homey.alert( String message[, String icon] ): Promise<void>` Show an alert dialog. The second parameter `icon` can be `null`, `error`, `warning` or `info`.

### Confirm dialog

`Homey.confirm( String message[, String icon] ): Promise<boolean>` Show a confirm dialog. The second parameter `icon` can be `null`, `error`, `warning` or `info`.

The promise will resolve to `true` when the user pressed `OK`.

### Popup

`Homey.popup( String url )` Show a popup with a remote website.

### Internationalisation

`Homey.__( String key [, Object tokens] )`

Translate a string programmatically. The first argument `key` is the name in your `/locales/__language__.json`. Use dots to get a sub-property, e.g. `settings.title`. The optional second argument `tokens` is an object with replacers.

{% code title="/drivers/\<driver\_id>/pair/start.html" %}

```markup
<script type="application/javascript">
  function onHomeyReady(Homey) {
    alert(Homey.__("pair.title")); // will alert "Settings page title"
  }
</script>
```

{% endcode %}

Within your custom views, you can also use translations. For example:

{% code title="/drivers/\<driver\_id>/pair/start.html" %}

```markup
<span data-i18n="pair.title"></span>
<p data-i18n="pair.intro"></p>
```

{% endcode %}

Read more about translations in the [internationalization guide](/the-basics/app/internationalization).

### Show the loading overlay

`Homey.showLoadingOverlay()`

Shows the loading overlay.

### Hide the loading overlay

`Homey.hideLoadingOverlay()`

Hides the loading overlay.

### Get a view's store value

`Homey.getViewStoreValue( String viewId, String key ): Promise<any>`

Get's a view's store value. Promise will resolve to the requested value.

### Set a view's store value

`Homey.setViewStoreValue( String viewId, String key, Mixes value): Promise<void>`

Set a view's store value.

#### Example

{% code title="/drivers/\<driver\_id>/pair/start.html" %}

```markup
<script type="application/javascript">
  var devicesArray = [
    {
      name: "My Device",
      data: {
        id: "abcd",
      },
    },
  ];
  Homey.setViewStoreValue("add_devices", "devices", devicesArray);
</script>
```

{% endcode %}


# HTML & CSS Styling

The Homey Style Library is the key to a consistent user experience across all Homey Apps. We recommend to use this library above custom styling.

{% hint style="info" %}
These CSS classes are available on Homey Cloud, and on Homey Pro since <mark style="background-color:blue;">v8.1.0</mark>
{% endhint %}

## Header and Titles

For custom pairing screens and app settings you might want to use our default header with title and an optional subtitle.

<table><thead><tr><th width="253">CSS class</th><th>HTML</th></tr></thead><tbody><tr><td><code>.homey-header</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;header class="homey-header">&#x3C;/header>
</code></pre></td></tr><tr><td><code>.homey-title</code></td><td><pre class="language-html"><code class="lang-html">&#x3C;h1 class="homey-title">&#x3C;/h1>
</code></pre></td></tr><tr><td><code>.homey-subtitle</code></td><td><pre class="language-html"><code class="lang-html">&#x3C;p class="homey-subtitle">&#x3C;/p>
</code></pre></td></tr></tbody></table>

#### Example

<div align="left"><figure><img src="/files/kWgp0v9D3fLgeuzpsoLQ" alt=""><figcaption><p>By using the <code>homey-header</code> class you create spacing and a line between your settings and title(s).</p></figcaption></figure></div>

{% code lineNumbers="true" %}

```html
<header class="homey-header">
  <h1 class="homey-title" data-i18n="settings.title">
    <!-- This will be filled with the translated string with key 'settings.title'. -->
  </h1>
  <p class="homey-subtitle" data-i18n="settings.subtitle">
    <!-- This will be filled with the translated string with key 'settings.subtitle'. -->
  </p>
</header>
```

{% endcode %}

## Forms

Start a new form by using the `homey-form` CSS class.

<table><thead><tr><th width="253">CSS class</th><th>HTML</th></tr></thead><tbody><tr><td><code>.homey-form</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;form class="homey-form">&#x3C;/form>
</code></pre></td></tr></tbody></table>

#### Example

```html
<form class="homey-form">
 <!-- Your form html here -->
</form>
```

### Fieldset and legend

With `<fieldset class="homey-form-fieldset">` you can create a new fieldset in your form.\
A fieldset is useful to create larger sections in your forms. Make sure your always use a `<legend class="homey-form-legend">` to give the fieldset a title.

<table><thead><tr><th width="253">CSS class</th><th>HTML</th></tr></thead><tbody><tr><td><code>.homey-form-fieldset</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;fieldset class="homey-form-fieldset">&#x3C;/fieldset>
</code></pre></td></tr><tr><td><code>.homey-form-legend</code></td><td><pre class="language-html"><code class="lang-html">&#x3C;legend class="homey-form-legend">&#x3C;/legend>
</code></pre></td></tr></tbody></table>

#### Example

<div align="left"><figure><img src="/files/hK5ByzdhZgdl3bDZLTwA" alt=""><figcaption><p>Use a <code>homey-form-legend</code> to create headings between form elements.</p></figcaption></figure></div>

{% code lineNumbers="true" %}

```html
<fieldset class="homey-form-fieldset">
  <legend class="homey-form-legend"></legend>
  <!-- ... -->
</fieldset>
```

{% endcode %}

### Groups

Use `<div class="homey-form-group">` to create a group combining a label with an input field. Use the `homey-form-group` class to create equal vertical spacing between all inputs.

<table><thead><tr><th width="231">CSS class</th><th>HTML</th></tr></thead><tbody><tr><td><code>.homey-form-group</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;div class="homey-form-group">&#x3C;/div>
</code></pre></td></tr></tbody></table>

#### Example

<div align="left"><figure><img src="/files/llF4G3CbL1PDHDDMbkhT" alt=""><figcaption><p>The yellow space marks the spacing above and below each <code>.homey-form-group</code> class</p></figcaption></figure></div>

<pre class="language-html" data-line-numbers><code class="lang-html"><strong>&#x3C;div class="homey-form-group">
</strong>  &#x3C;label class="homey-form-label" for="target">label&#x3C;/label>
  &#x3C;input class="homey-form-input" id="target" type="text" value=""/>
<strong>&#x3C;/div>
</strong></code></pre>

### Basic input & label

<table><thead><tr><th width="231">CSS class</th><th>HTML</th></tr></thead><tbody><tr><td><code>.homey-form-label</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;label class="homey-form-label" for="target">&#x3C;/label>
</code></pre></td></tr><tr><td><code>.homey-form-input</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;input class="homey-form-input" id="target" type="text" value=""/>
</code></pre></td></tr><tr><td></td><td>Can be used for the input types: <code>text</code>, <code>number</code>, <code>password</code> and <code>url</code> .</td></tr></tbody></table>

#### Example

<div align="left"><figure><img src="/files/E9fGIoKzEYpLhMjQApK3" alt=""><figcaption><p>Basic label + input example</p></figcaption></figure></div>

<pre class="language-html" data-line-numbers><code class="lang-html">&#x3C;form class="homey-form">
  &#x3C;fieldset class="homey-form-fieldset">
    &#x3C;legend class="homey-form-legend">Login data&#x3C;/legend>

    &#x3C;div class="homey-form-group">
<strong>      &#x3C;label class="homey-form-label" for="username">Username&#x3C;/label>
</strong><strong>      &#x3C;input class="homey-form-input" id="username" type="text" value=""/>
</strong>    &#x3C;/div>
    &#x3C;div class="homey-form-group">
<strong>      &#x3C;label class="homey-form-label" for="password">Password&#x3C;/label>
</strong><strong>      &#x3C;input class="homey-form-input" id="password" type="password" value=""/>
</strong>    &#x3C;/div>
    &#x3C;!-- ... -->
</code></pre>

### Radio input

#### Radio set

<table><thead><tr><th width="231">CSS class</th><th>HTML</th></tr></thead><tbody><tr><td><code>.homey-form-radio-set</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;fieldset class="homey-form-radio-set">&#x3C;/fieldset>
</code></pre></td></tr><tr><td><code>.homey-form-radio-set-title</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;legend class="homey-form-radio-set-title">&#x3C;/legend>
</code></pre></td></tr></tbody></table>

#### Radio buttons

<table><thead><tr><th width="323">CSS class</th><th>HTML</th></tr></thead><tbody><tr><td><code>.homey-form-radio</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;label class="homey-form-radio">&#x3C;/label>
</code></pre></td></tr><tr><td><code>.homey-form-radio-input</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;input class="homey-form-radio-input">&#x3C;/input>
</code></pre></td></tr><tr><td><code>.homey-form-radio-checkmark</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;span class="homey-form-radio-checkmark">&#x3C;/span>
</code></pre></td></tr><tr><td><code>.homey-form-radio-text</code></td><td><pre class="language-html"><code class="lang-html">&#x3C;span class="homey-form-radio-text">&#x3C;/span>
</code></pre></td></tr></tbody></table>

#### Example

<div align="left"><figure><img src="/files/c5KoJvGK1D8gMZiKDj2D" alt=""><figcaption><p>Radio buttons should be grouped in fieldsets. You can use multiple levels of fieldsets with different classes.</p></figcaption></figure></div>

{% code lineNumbers="true" %}

```html
<form class="homey-form">
  <!-- ... -->
  <fieldset class="homey-form-fieldset">
    <legend class="homey-form-legend">Multiple choice questions</legend>
    
    <div class="homey-form-group">
      <fieldset class="homey-form-radio-set">
        <legend class="homey-form-radio-set-title">Group of radio buttons</legend>
    
        <label class="homey-form-radio">
          <input class="homey-form-radio-input" type="radio" name="radio-example"/>
          <span class="homey-form-radio-checkmark"></span>
          <span class="homey-form-radio-text">Radio label 1</span>
        </label>
    
        <label class="homey-form-radio">
          <input class="homey-form-radio-input" type="radio" name="radio-example"/>
          <span class="homey-form-radio-checkmark"></span>
          <span class="homey-form-radio-text">Radio label 2</span>
        </label>
    
        <label class="homey-form-radio">
          <input class="homey-form-radio-input" type="radio" name="radio-example"/>
          <span class="homey-form-radio-checkmark"></span>
          <span class="homey-form-radio-text">Radio label 3</span>
        </label>
      </fieldset>
    </div>
    <!-- ... -->
```

{% endcode %}

### Checkbox input

#### Checkbox set

<table><thead><tr><th width="350">CSS class</th><th>HTML</th></tr></thead><tbody><tr><td><code>.homey-form-checkbox-set</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;fieldset class="homey-form-checkbox-set">&#x3C;/fieldset>
</code></pre></td></tr><tr><td><code>.homey-form-checkbox-set-title</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;legend class="homey-form-radio-checkbox-set-title">&#x3C;/legend>
</code></pre></td></tr></tbody></table>

#### Checkboxes

<table><thead><tr><th width="323">CSS class</th><th>HTML</th></tr></thead><tbody><tr><td><code>.homey-form-checkbox</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;label class="homey-form-checkbox">&#x3C;/label>
</code></pre></td></tr><tr><td><code>.homey-form-checkbox-input</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;input class="homey-form-checkbox-input">&#x3C;/input>
</code></pre></td></tr><tr><td><code>.homey-form-checkbox-checkmark</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;span class="homey-form-checkbox-checkmark">&#x3C;/span>
</code></pre></td></tr><tr><td><code>.homey-form-checkbox-text</code></td><td><pre class="language-html"><code class="lang-html">&#x3C;span class="homey-form-checkbox-text">&#x3C;/span>
</code></pre></td></tr></tbody></table>

#### Example

<div align="left"><figure><img src="/files/4zurCMX0cx5iY0sm1ZEh" alt=""><figcaption></figcaption></figure></div>

{% code lineNumbers="true" %}

```html
<div class="homey-form-group">
  <fieldset class="homey-form-checkbox-set">
    <legend class="homey-form-checkbox-set-title">Group of checkbox buttons</legend>

    <label class="homey-form-checkbox">
      <input class="homey-form-checkbox-input" type="checkbox" name="checkbox-example"/>
      <span class="homey-form-checkbox-checkmark"></span>
      <span class="homey-form-checkbox-text">Checkbox label 1</span>
    </label>

    <label class="homey-form-checkbox">
      <input class="homey-form-checkbox-input" type="checkbox" name="checkbox-example"/>
      <span class="homey-form-checkbox-checkmark"></span>
      <span class="homey-form-checkbox-text">Checkbox label 2</span>
    </label>

    <label class="homey-form-checkbox">
      <input class="homey-form-checkbox-input" type="checkbox" name="checkbox-example"/>
      <span class="homey-form-checkbox-checkmark"></span>
      <span class="homey-form-checkbox-text">Checkbox label 3</span>
    </label>
  </fieldset>
</div>
```

{% endcode %}

### Select

<table><thead><tr><th width="246">CSS class</th><th>HTML</th></tr></thead><tbody><tr><td><code>.homey-form-select</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;select class="homey-form-select">
  &#x3C;option value="1">Option 1&#x3C;/option>
&#x3C;/select>
</code></pre></td></tr></tbody></table>

#### Example

<div align="left"><figure><img src="/files/AremiB4qhYaN1a8vgsDf" alt=""><figcaption></figcaption></figure></div>

{% code lineNumbers="true" %}

```html
<div class="homey-form-group">
  <label class="homey-form-label" for="select-example">Select your option</label>
  <select class="homey-form-select" name="select-example" id="select-example">
    <option value="1">Option 1</option>
    <option value="2">Option 2</option>
    <option value="3">Option 3</option>
  </select>
</div>
```

{% endcode %}

### Textarea

<table><thead><tr><th width="246">CSS class</th><th>HTML</th></tr></thead><tbody><tr><td><code>.homey-form-textarea</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;textarea class="homey-form-textarea">&#x3C;/textarea>
</code></pre></td></tr></tbody></table>

#### Example

<div align="left"><figure><img src="/files/e4JsZQgMeDCNQRlbCcTH" alt=""><figcaption></figcaption></figure></div>

{% code lineNumbers="true" %}

```html
<div class="homey-form-group">
  <label for="textarea-example-1" class="homey-form-label">Label for textarea</label>
  <textarea class="homey-form-textarea" name="textarea-example-1" id="textarea-example-1" rows="10"
            placeholder="type here your text"></textarea>
</div>
```

{% endcode %}

## Buttons

### Button variants

{% hint style="info" %}
**Using multiple button variants:**\
Each button class starts with `.homey-button` this you can follow up with *color variants* such as: `-primary` ,`-secondary`, `-danger`. You can further adjust the button style by adding `-full`, `-shadow`, `-small` parameters. This way you end up having a class such as `.homey-button-primary-shadow-full`.
{% endhint %}

<table><thead><tr><th width="383">CSS class</th><th>HTML</th></tr></thead><tbody><tr><td><code>.homey-button-primary</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;button class="homey-button-primary">&#x3C;/button>
</code></pre></td></tr><tr><td><code>.homey-button-primary-full</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;button class="homey-button-primary-full">&#x3C;/button>
</code></pre></td></tr><tr><td><code>.homey-button-primary-shadow</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;button class="homey-button-primary-shadow">&#x3C;/button>
</code></pre></td></tr><tr><td><code>.homey-button-primary-shadow-full</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;button class="homey-button-primary-shadow-full">&#x3C;/button>
</code></pre></td></tr><tr><td><code>.homey-button-transparent</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;button class="homey-button-transparent">&#x3C;/button>
</code></pre></td></tr><tr><td><p><mark style="background-color:blue;">v8.1.1</mark></p><p><code>.homey-button-secondary-shadow</code></p></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;button class="homey-button-secondary-shadow">&#x3C;/button>
</code></pre></td></tr><tr><td><p><mark style="background-color:blue;">v8.1.1</mark></p><p><code>.homey-button-danger-shadow</code></p></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;button class="homey-button-danger-shadow">&#x3C;/button>
</code></pre></td></tr><tr><td><p><mark style="background-color:blue;">v8.1.1</mark></p><p><code>.homey-button-small</code></p></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;button class="homey-button-small">&#x3C;/button>
</code></pre></td></tr></tbody></table>

#### Example

<div align="left"><figure><img src="/files/fjPHWXi6qdMtg7sUaHfK" alt=""><figcaption><p><code>.homey-button-primary-full</code></p></figcaption></figure> <figure><img src="/files/hXf4yMe1LT77xZV4qeJc" alt=""><figcaption><p><code>.homey-button-secondary-shadow</code></p></figcaption></figure></div>

<div align="left"><figure><img src="/files/x6LFi8B5d0uc456Ge3bE" alt=""><figcaption><p><code>.homey-button-danger-shadow</code></p></figcaption></figure></div>

###

### Disabled state

<table><thead><tr><th width="365">CSS class</th><th>HTML</th></tr></thead><tbody><tr><td><code>.homey-button-</code>{<code>variant}.is-disabled</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;button class="homey-button-primary is-disabled">&#x3C;/button>
</code></pre></td></tr><tr><td><code>.homey-button-{variant}[disabled=disabled]</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;button class="homey-button-primary" disabled="disabled">&#x3C;/button>
</code></pre></td></tr></tbody></table>

#### Example

<div align="left"><figure><img src="/files/c0OP1FKgiFCLqYgYZa8o" alt=""><figcaption><p><code>.homey-button-primary-full.is-disabled</code></p></figcaption></figure></div>

### Loading state

<table><thead><tr><th width="365">CSS class</th><th>HTML</th></tr></thead><tbody><tr><td><code>.homey-button-primary-{variant}.is-loading</code></td><td><pre class="language-html" data-overflow="wrap"><code class="lang-html">&#x3C;button class="homey-button-primary-full is-loading">&#x3C;/button>
</code></pre></td></tr></tbody></table>

#### Example

<div align="left"><figure><img src="/files/dqJN6MCXSw45cTZdu33k" alt=""><figcaption><p><code>.homey-button-primary-full.is-loading</code></p></figcaption></figure></div>

## Right-to-Left (RTL) Styling

Homey supports Right-to-Left (RTL) layouts for languages such as Arabic. RTL layout direction is handled automatically by Homey, but custom views may require additional styling to ensure the interface remains clear and consistent in RTL contexts.

When styling custom views, keep RTL in mind from the start to avoid layout issues.

### Recommended practices

Use direction-aware CSS wherever possible so your styles work in both Left-to-Right (LTR) and Right-to-Left (RTL) layouts.

For general information about RTL support and language localization, see [Internationalization](/the-basics/app/internationalization#right-to-left-rtl-and-arabic-language-support).

#### **Prefer logical properties over left/right**

Logical properties automatically adapt to the active text direction.

```css
/* Preferred */
padding-inline-start: 16px;
padding-inline-end: 16px;
margin-inline-start: 8px;

/* Avoid */
padding-left: 16px;
padding-right: 16px;
margin-left: 8px;
```

#### **Use direction-aware text alignment**

```css
text-align: start;
```

Avoid hard-coding left or right unless absolutely necessary.

#### **Use :dir(rtl)for direction-specific adjustments**

Some visuals are inherently directional and may require explicit RTL handling.

```css
.chevron:dir(rtl) {
  transform: scaleX(-1);
}
```

Common cases include:

* arrows and chevrons
* animations and transitions
* progress indicators
* absolute positioning

**Be careful with absolute positioning**

If possible, use logical positioning properties:

```css
inset-inline-start: 0;
```

Only fall back to left / right with a :dir(rtl) override when needed.


# Web API

Exposing and consuming a custom REST API for your Homey App with support for real-time events.

A Homey app can add its own endpoints to Homey's Web API (REST + Realtime), to allow for external access. As an example, you could use this API to enable a Raspberry PI to reports its status to Homey.

{% hint style="info" %}
Apps on Homey Cloud are not allowed to expose a Web API. Read more about this in the [Homey Cloud guide](/guides/homey-cloud).
{% endhint %}

Your app's API endpoints are available under the following url: `/api/app/com.yourapp.id/`. All endpoints are protected by default, and the requesting user needs permission to your app (which is granted by default after installation). You can override this by setting `"public": true`.

To add API endpoints to an app, start by defining the routes in the [App Manifest](/the-basics/app/manifest). The key of each route corresponds to the name of a function you define. The following route options can be configured in the App Manifest:

| key      | type              | value                                                                             |
| -------- | ----------------- | --------------------------------------------------------------------------------- |
| `method` | `String`, `Array` | `"GET"`, `"POST"`, `"PUT"` or `"DELETE"`, or an array of these values.            |
| `path`   | `String`          | for example `"/"`, `"/:foo"`, `"/bar/:foo"`                                       |
| `public` | `Boolean`         | Default: `false`, set to `true` to make this endpoint accessible without a token. |

{% hint style="warning" %}
Only use public endpoints when no alternatives are possible. A good usecase for a public endpoint is sending a pin-code from another device to Homey.
{% endhint %}

{% tabs %}
{% tab title="JavaScript" %}
In the following example we define four routes named `getSomething`, `addSomething`, `updateSomething` and `deleteSomething` in `api.js`:

{% code title="/.homeycompose/app.json" %}

```javascript
  "api": {
    "getSomething": {
      "method": "GET",
      "path": "/"
    },
    "addSomething": {
      "method": "POST",
      "path": "/"
    },
    "updateSomething": {
      "method": "PUT",
      "path": "/:id"
    },
    "deleteSomething": {
      "method": "DELETE",
      "path": "/:id"
    }
  }
```

{% endcode %}

The implementation of each route is defined in the `api.js` file, this file should export async functions with names that correspond to the names defined in the App Manifest. For example:

{% code title="/api.js" %}

```javascript
module.exports = {
  async getSomething({ homey, query }) {
    // you can access query parameters like "/?foo=bar" through `query.foo`

    // you can access the App instance through homey.app
    const result = await homey.app.getSomething();

    // perform other logic like mapping result data

    return result;
  },

  async addSomething({ homey, body }) {
    // access the post body and perform some action on it.
    return homey.app.addSomething(body);
  },

  async updateSomething({ homey, params, body }) {
    return homey.app.updateSomething(params.id, body);
  },

  async deleteSomething({ homey, params }) {
    return homey.app.deleteSomething(params.id);
  },
};
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
In the following example we define four routes named `getSomething`, `addSomething`, `updateSomething` and `deleteSomething` in `api.mts`:

{% code title="/.homeycompose/app.json" %}

```javascript
  "api": {
    "getSomething": {
      "method": "GET",
      "path": "/"
    },
    "addSomething": {
      "method": "POST",
      "path": "/"
    },
    "updateSomething": {
      "method": "PUT",
      "path": "/:id"
    },
    "deleteSomething": {
      "method": "DELETE",
      "path": "/:id"
    }
  }
```

{% endcode %}

The implementation of each route is defined in the `api.mts` file, this file should export async functions with names that correspond to the names defined in the App Manifest. For example:

{% code title="/api.mts" %}

```mts
import type App from "./app.mjs";

type RequestWithBody = {
  homey: App["homey"];
  query: Record<string, string>;
  params: Record<string, string>;
  body: Record<string, unknown>;
};

type RequestWithoutBody = {
  homey: App["homey"];
  query: Record<string, string>;
  params: Record<string, string>;
  body: Record<never, never>; // Homey.API sends an empty body for GET and DELETE requests
};

export default {
  async getSomething({ homey, query }: RequestWithoutBody): Promise<any> {
    // you can access query parameters like "/?foo=bar" through `query.foo`

    // you can access the App instance through homey.app
    const result = await (homey.app as App).getSomething();

    // perform other logic like mapping result data

    return result;
  },

  async addSomething({ homey, body }: RequestWithBody): Promise<any> {
    // access the post body and perform some action on it.
    return (homey.app as App).addSomething(body);
  },

  async updateSomething({ homey, params, body }: RequestWithBody): Promise<any> {
    return (homey.app as App).updateSomething(params.id, body);
  },

  async deleteSomething({ homey, params }: RequestWithoutBody): Promise<any> {
    return (homey.app as App).deleteSomething(params.id);
  },
};

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
In the following example we define four routes named `get_something`, `add_something`, `update_something` and `delete_something` in `api.py`:

{% code title="/.homeycompose/app.json" %}

```javascript
  "api": {
    "get_something": {
      "method": "GET",
      "path": "/"
    },
    "add_something": {
      "method": "POST",
      "path": "/"
    },
    "update_something": {
      "method": "PUT",
      "path": "/:id"
    },
    "delete_something": {
      "method": "DELETE",
      "path": "/:id"
    }
  }
```

{% endcode %}

The implementation of each route is defined in the `api.py` file, this file should export async functions with names that correspond to the names defined in the App Manifest. For example:

{% code title="/api.py" %}

```python
from typing import Any, Never, cast

from homey.homey import Homey

from .app import App


async def get_something(
    *,
    homey: Homey,
    query: dict[str, str],
    params: dict[str, str],
    body: dict[Never, Never],  # Homey.API sends an empty body for GET requests
) -> Any:
    # you can access query parameters like "/?foo=bar" through `query.get("foo")`

    # you can access the App instance through homey.app
    result = cast(App, homey.app).get_something()

    # perform other logic like mapping result data

    return result


async def add_something(
    *, homey: Homey, query: dict[str, str], params: dict[str, str], body: dict[str, Any]
) -> Any:
    return cast(App, homey.app).add_something(body)


async def update_something(
    *, homey: Homey, query: dict[str, str], params: dict[str, str], body: dict[str, Any]
) -> Any:
    return cast(App, homey.app).update_something(body)


async def delete_something(
    *,
    homey: Homey,
    query: dict[str, str],
    params: dict[str, str],
    body: dict[Never, Never],  # Homey.API sends an empty body for DELETE requests
) -> Any:
    return cast(App, homey.app).delete_something(params["id"])


# Export all these methods as endpoints
__all__ = ["get_something", "add_something", "update_something", "delete_something"]

```

{% endcode %}
{% endtab %}
{% endtabs %}

Api functions receive an object as their argument, this object has four properties: `homey`, `params`, `query` and `body`.

* `homey` is the Homey instance. Using this instance you can, for example, access the App instance.
* `body` is an object with the request body, when your request has method `POST` or `PUT`. JSON is automatically parsed.
* `params` is a set of strings defined in your `path`.
* `query` is a set of strings that are provided as query parameters, for example `?foo=bar` will result in `{ "foo": "bar" }`.

## Realtime events

Your app can emit 'realtime' events, which are one-way events to a subscribing client, for example a browser showing a settings view page.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  async onInit() {
    await this.homey.api.realtime("my_event", "my_json_stringifyable_value");
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/app.mts" %}

```mts
import Homey from "homey";

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    await this.homey.api.realtime("my_event", "my_json_stringifyable_value");
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/app.py" %}

```python
from homey import app


class App(app.App):
    async def on_init(self) -> None:
        await self.homey.api.realtime("my_event", "my_json_stringifyable_value")


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}

## Accessing the Web API of another app

Apps can also talk to each other through their API's, however you need to define the correct permissions first. Permissions for app to app communication look like this `homey:app:<appId>`, for example `homey:app:com.athom.example` or `homey:app:com.yahoo.weather`.

In order to communicate with another app you first need to create a `ApiApp` client.

The Homey Apps SDK provides some information about the app you are connecting to. For example whether it is installed on the Homey your app is installed on and what version of the app is installed. You can even subscribe to events that get emitted when an app is installed or uninstalled.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  async onInit() {
    this.otherAppApi = this.homey.api.getApiApp('com.athom.otherApp');

    const isInstalled = await this.otherAppApi.getInstalled();
    const version = await this.otherAppApi.getVersion();

    this.otherAppApi.on('install', () => {
      console.log('otherApp is installed');
    });

    this.otherAppApi.on('uninstall', () => {
      console.log('otherApp is uninstalled');
    });
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/app.mts" %}

```mts
import Homey, { ApiApp } from "homey";

export default class App extends Homey.App {
  otherAppApi!: ApiApp;

  async onInit(): Promise<void> {
    this.otherAppApi = this.homey.api.getApiApp("com.athom.otherApp");

    const isInstalled = await this.otherAppApi.getInstalled();
    const version = await this.otherAppApi.getVersion();

    this.otherAppApi.on("install", () => {
      console.log("otherApp is installed");
    });

    this.otherAppApi.on("uninstall", () => {
      console.log("otherApp is uninstalled");
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/app.py" %}

```python
from homey import app
from homey.api_app import ApiApp


class App(app.App):
    other_app_api: ApiApp

    async def on_init(self) -> None:
        self.other_app_api = self.homey.api.get_api_app("com.athom.otherApp")

        is_installed = await self.other_app_api.get_installed()
        version = await self.other_app_api.get_version()

        def on_install():
            print("otherApp is installed")

        self.other_app_api.on_install(on_install)

        def on_uninstall():
            print("otherApp is uninstalled")

        self.other_app_api.on_uninstall(on_uninstall)


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}

{% hint style="info" %}
Always check whether the target app is installed and has a compatible version before trying to send requests. Failing to do so may cause your app to break unexpectedly when the target app is updated.
{% endhint %}

You can interact with the Web API of the target app by making requests and listening to realtime events. Which APIs and events you can expect is up to the target app, so make sure to check the source code, read the documentation or ask the app's developer.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  async onInit() {
    this.otherAppApi = this.homey.api.getApiApp("com.athom.otherApp");

    // Make a get request to "otherApp"s API
    const getResponse = await this.otherAppApi.get('/');

    // Post some data to "otherApp", the second argument is the request body
    const postResponse = await this.otherAppApi.post('/play', { sound: 'bell' });

    // Listen to app realtime events
    this.otherAppApi.on('realtime', (event) => {
      console.log('otherApp.onRealtime', event);
    });
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/app.mts" %}

```mts
import Homey, { ApiApp } from "homey";

export default class App extends Homey.App {
  otherAppApi!: ApiApp;

  async onInit(): Promise<void> {
    this.otherAppApi = this.homey.api.getApiApp("com.athom.otherApp");

    // Make a get request to "otherApp"s API
    const getResponse = await this.otherAppApi.get("/");

    // Post some data to "otherApp", the second argument is the request body
    const postResponse = await this.otherAppApi.post("/play", { sound: "bell" });

    // Listen to app realtime events
    this.otherAppApi.on("realtime", event => {
      console.log("otherApp.onRealtime", event);
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/app.py" %}

```python
from homey import app
from homey.api_app import ApiApp


class App(app.App):
    other_app_api: ApiApp

    async def on_init(self) -> None:
        self.other_app_api = self.homey.api.get_api_app("com.athom.otherApp")

        # Make a get request to "otherApp"s API
        get_response = await self.other_app_api.get("/")

        # Post some data to "otherApp", the second argument is the request body
        post_response = await self.other_app_api.post("/play", {"sound": "bell"})

        # Listen to app realtime events
        def on_realtime(event, *args):
            print("otherApp.onRealtime", event)

        self.other_app_api.on_realtime(on_realtime)


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}


# Images

Images can be used in various places throughout Homey, such as album art for speakers, camera devices and in Flows.

When an `Image` is created, it needs a way of providing Homey with its data. This can be either:

* an URL, available from anywhere on the internet
* a binary stream through the `getStream` method
* a local path to a static image which is shipped with the App.

{% hint style="warning" %}
Note: Images are limited to 5 MB.
{% endhint %}

{% hint style="info" %}
You can debug your images in the [Developer Tools](https://tools.developer.homey.app/tools/images).
{% endhint %}

## Creating an image

### Using an URL

URLs should be used when the image is available publicly on the internet.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  async onInit() {
    const myImage = await this.homey.images.createImage();
    // the URL must start with https://
    myImage.setUrl("https://www.example.com/image.png");
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/app.mts" %}

```mts
import Homey from "homey";

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    const myImage = await this.homey.images.createImage();
    // the URL must start with https://
    myImage.setUrl("https://www.example.com/image.png");
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/app.py" %}

```python
from homey import app


class App(app.App):
    async def on_init(self) -> None:
        my_image = await self.homey.images.create_image()
        # the URL must start with https://
        my_image.set_url("https://www.example.com/image.png")


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}

### Using a Stream

{% tabs %}
{% tab title="JavaScript" %}
Streams should be used when downloading an image that cannot be supplied using [`Image#setUrl()`](https://apps-sdk-v3.developer.homey.app/Image.html#setUrl). Using image streams involves writing data directly into a Node.js stream. Using streams requires homey version 2.2.0 or higher.

{% code title="/app.js" %}

```javascript
const Homey = require('homey');
const fetch = require("node-fetch");

class App extends Homey.App {
  async onInit() {
    const myImage = await this.homey.images.createImage();

    myImage.setStream(async (stream) => {
      const res = await fetch("http://192.168.1.100/image.png");
      if (!res.ok) {
        throw new Error("Invalid Response");
      }

      return res.body.pipe(stream);
    });
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
Streams should be used when downloading an image that cannot be supplied using [`Image#setUrl()`](https://apps-sdk-v3.developer.homey.app/Image.html#setUrl). Using image streams involves writing data directly into a Node.js stream. Using streams requires homey version 2.2.0 or higher.

{% code title="/app.mts" %}

```mts
import Homey from "homey";
import fetch from "node-fetch";
import type { Writable } from "stream";

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    const myImage = await this.homey.images.createImage();

    myImage.setStream(async (stream: Writable) => {
      const res = await fetch("http://192.168.1.100/image.png");
      if (!res.ok || res.body === null) {
        throw new Error("Invalid Response");
      }

      return res.body.pipe(stream);
    });
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
Streams should be used when downloading an image that cannot be supplied using [`Image#set_url()`](https://python-apps-sdk-v3.developer.homey.app/image.html#homey.image.Image.set_url). Using image streams involves writing data directly into a io.BytesIO stream. Using streams requires homey version 2.2.0 or higher.

{% code title="/app.py" %}

```python
from io import BytesIO

import aiohttp
from homey import app


class App(app.App):
    async def on_init(self) -> None:
        my_image = await self.homey.images.create_image()

        async def stream_image(stream: BytesIO):
            async with aiohttp.ClientSession() as session:
                async with session.get("http://192.168.1.100/image.png") as res:
                    if not res.ok:
                        raise Exception("Invalid Response")
                    stream.write(await res.read())

        my_image.set_stream(stream_image)


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}

### Using a Path

Paths should be used when the image is locally available.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  async onInit() {
    const myImage = await this.homey.images.createImage();
    myImage.setPath("/userdata/image.png");
  }
}

module.exports = App;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/app.mts" %}

```mts
import Homey from "homey";

export default class App extends Homey.App {
  async onInit(): Promise<void> {
    const myImage = await this.homey.images.createImage();
    myImage.setPath("/userdata/image.png");
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/app.py" %}

```python
from homey import app


class App(app.App):
    async def on_init(self) -> None:
        my_image = await self.homey.images.create_image()
        my_image.set_path("/userdata/image.png")


homey_export = App

```

{% endcode %}
{% endtab %}
{% endtabs %}

## Updating the image

{% tabs %}
{% tab title="JavaScript" %}
Call [`Image#update()`](https://apps-sdk-v3.developer.homey.app/Image.html#update) when the image has been updated, and the front-end will download the image again.

When your image uses a Stream, the method provided in [`Image#setStream()`](https://apps-sdk-v3.developer.homey.app/Image.html#setStream) will be called again.

At any time, you can switch between delivery type by calling [`Image#setPath()`](https://apps-sdk-v3.developer.homey.app/Image.html#setPath), [`Image#setStream()`](https://apps-sdk-v3.developer.homey.app/Image.html#setStream) or [`Image#setURL()`](https://apps-sdk-v3.developer.homey.app/Image.html#setURL).
{% endtab %}

{% tab title="TypeScript" %}
Call [`Image#update()`](https://apps-sdk-v3.developer.homey.app/Image.html#update) when the image has been updated, and the front-end will download the image again.

When your image uses a Stream, the method provided in [`Image#setStream()`](https://apps-sdk-v3.developer.homey.app/Image.html#setStream) will be called again.

At any time, you can switch between delivery type by calling [`Image#setPath()`](https://apps-sdk-v3.developer.homey.app/Image.html#setPath), [`Image#setStream()`](https://apps-sdk-v3.developer.homey.app/Image.html#setStream) or [`Image#setURL()`](https://apps-sdk-v3.developer.homey.app/Image.html#setURL).
{% endtab %}

{% tab title="Python" %}
Call [`Image#update()`](https://python-apps-sdk-v3.developer.homey.app/image.html#homey.image.Image.update) when the image has been updated, and the front-end will download the image again.

When your image uses a Stream, the method provided in [`Image#set_stream()`](https://python-apps-sdk-v3.developer.homey.app/image.html#homey.image.Image.set_stream) will be called again.

At any time, you can switch between delivery type by calling [`Image#set_path()`](https://python-apps-sdk-v3.developer.homey.app/image.html#homey.image.Image.set_path), [`Image#set_stream()`](https://python-apps-sdk-v3.developer.homey.app/image.html#homey.image.Image.set_stream) or [`Image#set_url()`](https://python-apps-sdk-v3.developer.homey.app/image.html#homey.image.Image.set_url).
{% endtab %}
{% endtabs %}

## Retrieving an image

It is also possible to consume an image in your app, for instance through use of Flow Tokens.

```javascript
const { PassThrough } = require("stream");
const fetch = require("node-fetch");
const FormData = require("form-data");

//uploads an image to imgur and returns a link
async function uploadImage(image) {
  const stream = await image.getStream();

  const form = new FormData();

  form.append("image", stream, {
    contentType: stream.contentType,
    filename: stream.filename,
    name: "image",
  });

  form.append(
    "description",
    `This image can also be (temporarily) viewed at: ${image.cloudUrl} and ${image.localUrl}`
  );

  const response = await fetch("https://api.imgur.com/3/image", {
    method: "POST",
    //pipe through a passthrough stream, workarround for a node-fetch bug involving form-data streams without content length set.
    body: form.pipe(new PassThrough()),
    headers: {
      ...form.getHeaders(),
      Authorization: "Client-ID <YOUR_CLIENT_ID>",
    },
  });

  if (!response.ok) {
    throw new Error(response.statusText);
  }

  const { data } = await response.json();
  return data.link;
}
```


# Videos

Devices can add support for streaming video, which can be viewed from the mobile app & dashboards.

{% hint style="info" %}
Videos are available on Homey Pro (2023 - 2026) and Homey Pro mini since v12.7.0, and Homey Cloud.
{% endhint %}

Your app's devices can let Homey know that they support a video stream. Once a front-end (e.g. the Homey app for iOS & Android) requests to start watching, your app will receive a request to share the stream's details, such as URL and authentication. The app, nor Homey, does any transcoding, but acts as a broker between the camera and the front-end.

## Supported Video Types

Homey supports video streams with WebRTC, RTSP, RTMP, HLS, DASH. Other types *may* be supported. Because the Homey Mobile App embeds a VLC media player, you can always try to see if your video type works.

{% hint style="info" %}
Homey automatically serves all videos as WebRTC to the frontend as of Homey Pro (2023 - 2026), Homey Pro mini and Homey Self-Hosted Server version 12.12.0. This makes it possible to show the videos on the Homey Web App, as well as make the videos available outside of your local network. Developers can opt out of this by passing the `disableWebRTCProxy: true` option while creating the video (see [Apps SDK - ManagerVideos](https://apps-sdk-v3.developer.homey.app/ManagerVideos.html))
{% endhint %}

## Getting Started with Videos

{% tabs %}
{% tab title="JavaScript" %}
To register a camera stream, your app needs to ask [ManagerVideos](https://apps-sdk-v3.developer.homey.app/ManagerVideos.html) first to create a video. Then, attach the video to your Device by calling `Device.setCameraVideo`. Note that when a device has both an image and video with the same `id` then the image will be used as a background image for the video while it is loading.
{% endtab %}

{% tab title="TypeScript" %}
To register a camera stream, your app needs to ask [ManagerVideos](https://apps-sdk-v3.developer.homey.app/ManagerVideos.html) first to create a video. Then, attach the video to your Device by calling `Device.setCameraVideo`. Note that when a device has both an image and video with the same `id` then the image will be used as a background image for the video while it is loading.
{% endtab %}

{% tab title="Python" %}
To register a camera stream, your app needs to ask [ManagerVideos](https://python-apps-sdk-v3.developer.homey.app/manager/videos.html#homey.manager.videos.ManagerVideos) first to create a video. Then, attach the video to your Device by calling `Device.set_camera_video`. Note that when a device has both an image and video with the same `id` then the image will be used as a background image for the video while it is loading.
{% endtab %}
{% endtabs %}

### Example — WebRTC

This example shows a basic WebRTC camera stream.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/drivers/my-webrtc-camera/device.mjs" %}

```javascript
import Homey from 'homey';

export default class MyWebRTCDevice extends Homey.Device {

    /*
     * WebRTC works by creating an offer SDP in the frontend, exchanging it for 
     * an answer SDP through the cameras API, and using that answer SDP in the
     * frontend to set up the connection.
     */
    async onInit() {
        try {
            const video = await this.homey.videos.createVideoWebRTC();

            /*
             * This listener is called when the user opens the camera stream in the
             * mobile app. The argument is an SDP offer generated by the mobile appp.
             */
            video.registerOfferListener(async (offerSdp) => {
                // Normally, you would call an API to exchange an SDP offer for an SDP answer
                const result = await this.oAuth2Client.createStream(offerSdp);
                return {
                    answerSdp: result.answerSdp,
                };
            });

            /*
             * Attach the camera to the device.
             */
            await this.setCameraVideo('main', 'Main Camera', video);
        } catch (err) {
            this.error('Error creating camera:', err);
        }   
    }
    
}
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/drivers/my-webrtc-camera/device.mts" %}

```mts
import Homey from "homey";

type WebRTCAnswer = {
  answerSdp: string;
  // Only needed if a keep-alive listener is used
  streamId?: string;
};

export default class Device extends Homey.Device {
  /*
   * WebRTC works by creating an offer SDP in the frontend, exchanging it for
   * an answer SDP through the cameras API, and using that answer SDP in the
   * frontend to set up the connection.
   */
  async onInit(): Promise<void> {
    try {
      const video = await this.homey.videos.createVideoWebRTC();

      /*
       * This listener is called when the user opens the camera stream in the
       * mobile app. The argument is an SDP offer generated by the mobile appp.
       */
      video.registerOfferListener(async (offerSdp: string): Promise<WebRTCAnswer> => {
        // Normally, you would call an API to exchange an SDP offer for an SDP answer
        const result = await this.oAuth2Client.createStream(offerSdp);
        return {
          answerSdp: result.answerSdp,
        };
      });

      /*
       * Attach the camera to the device.
       */
      await this.setCameraVideo("main", "Main Camera", video);
    } catch (err) {
      this.error("Error creating camera:", err);
    }
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/drivers/my-webrtc-camera/device.py" %}

```python
from homey import device
from homey.video_web_rtc import WebRTCAnswer

from .oauth2_client import OAuth2Client


class Device(device.Device):
    oauth2_client: OAuth2Client

    # WebRTC works by creating an offer SDP in the frontend, exchanging it for
    # an answer SDP through the cameras API, and using that answer SDP in the
    # frontend to set up the connection.
    async def on_init(self) -> None:
        try:
            video = await self.homey.videos.create_video_web_rtc()

            # This listener is called when the user opens the camera stream in the
            # mobile app. The argument is an SDP offer generated by the mobile app.
            async def offer_listener(offer: str) -> WebRTCAnswer:
                # Normally, you would call an API to exchange an SDP offer for an SDP answer
                result = await self.oauth2_client.create_stream(offer)
                return {
                    "answerSdp": result.get("answerSdp"),
                }

            video.register_offer_listener(offer_listener)

            # Attach the camera to the device.
            await self.set_camera_video("main", "Main Camera", video)
        except Exception as err:
            self.error("Error creating camera:", err)


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

### Example — WebRTC without Data Channel

This example shows a WebRTC camera without a data channel, and a keepalive listener.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/drivers/my-webrtc-camera/device.mjs" %}

```javascript
import Homey from 'homey';

export default class MyWebRTCDevice extends Homey.Device {

    /*
     * Some cameras require a data channel in order to work, while other cameras
     * only work when the offer does not contain a data channel. This can be
     * customized through the options object in the createCamera method.
     *
     * There are also cameras that only keep their stream open for a few minutes.
     * These can often be extended through an API call. The keep alive listener
     * can be used to send such a request.
     */
    async onInit() {
        try {
            const video = await this.homey.videos.createVideoWebRTC({
                dataChannel: false, // default: true
            });

            /*
             * The offer listener now also returns a stream ID next to the anwer
             * SDP. This stream ID can be used to identify the stream in the
             * keep alive listener.
             */
            video.registerOfferListener(async (offerSdp) => {
                // Normally, you would call an API to exchange an SDP offer for an SDP answer
                const result = await this.oAuth2Client.createStream(offerSdp);
                return {
                    answerSdp: result.answerSdp,
                    streamId: result.streamId,
                };
            });

            /*
             * The keep alive callback has a streamId argument that can be used
             * to identify the stream. Most APIs require such an identifier in
             * the request to extend the stream.
             */
            video.registerKeepAliveListener(async (streamId) => {
                // Normally, you would call an API to keep the stream alive
                await this.oAuth2Client.extendStream(streamId);
            });

            await this.setCameraVideo('main', 'Main Camera', video);
        } catch (err) {
            this.error('Error creating camera:', err);
        }   
    }
    
}
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/drivers/my-webrtc-camera/device.mts" %}

```mts
import Homey from "homey";

type WebRTCAnswer = {
  answerSdp: string;
  // Only needed if a keep-alive listener is used
  streamId?: string;
};

export default class Device extends Homey.Device {
  /*
   * Some cameras require a data channel in order to work, while other cameras
   * only work when the offer does not contain a data channel. This can be
   * customized through the options object in the createCamera method.
   *
   * There are also cameras that only keep their stream open for a few minutes.
   * These can often be extended through an API call. The keep alive listener
   * can be used to send such a request.
   */
  async onInit(): Promise<void> {
    try {
      const video = await this.homey.videos.createVideoWebRTC({
        dataChannel: false, // default: true
      });

      /*
       * The offer listener now also returns a stream ID next to the anwer
       * SDP. This stream ID can be used to identify the stream in the
       * keep alive listener.
       */
      video.registerOfferListener(async (offerSdp: string): Promise<WebRTCAnswer> => {
        // Normally, you would call an API to exchange an SDP offer for an SDP answer
        const result = await this.oAuth2Client.createStream(offerSdp);
        return {
          answerSdp: result.answerSdp,
          streamId: result.streamId,
        };
      });

      /*
       * The keep alive callback has a streamId argument that can be used
       * to identify the stream. Most APIs require such an identifier in
       * the request to extend the stream.
       */
      video.registerKeepAliveListener(async (streamId: string) => {
        // Normally, you would call an API to keep the stream alive
        await this.oAuth2Client.extendStream(streamId);
      });

      await this.setCameraVideo("main", "Main Camera", video);
    } catch (err) {
      this.error("Error creating camera:", err);
    }
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/drivers/my-webrtc-camera/device.py" %}

```python
from homey import device
from homey.video_web_rtc import WebRTCAnswer

from .oauth2_client import OAuth2Client


class Device(device.Device):
    oauth2_client: OAuth2Client

    """
    Some cameras require a data channel in order to work, while other cameras
    only work when the offer does not contain a data channel. This can be
    customized through the options object in the createCamera method.
    
    There are also cameras that only keep their stream open for a few minutes.
    These can often be extended through an API call. The keep alive listener
    can be used to send such a request.
    """

    async def on_init(self) -> None:
        try:
            video = await self.homey.videos.create_video_web_rtc()

            # The offer listener now also returns a stream ID
            # so it can be used to identify the stream in the keep alive listener.
            async def offer_listener(offer: str) -> WebRTCAnswer:
                # Normally, you would call an API to exchange an SDP offer for an SDP answer
                result = await self.oauth2_client.create_stream(offer)
                return {
                    "answerSdp": result.get("answerSdp"),
                    "streamId": result.get("streamId"),
                }

            video.register_offer_listener(offer_listener)

            # The keep alive callback has a stream_id argument that can be used
            # to identify the stream. Most APIs require such an identifier in
            # the request to extend the stream.
            async def keep_alive_listener(stream_id: str) -> None:
                await self.oauth2_client.extend_stream(stream_id)

            video.register_keep_alive_listener(keep_alive_listener)

            await self.set_camera_video("main", "Main Camera", video)
        except Exception as err:
            self.error("Error creating camera:", err)


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

### Example — RTSP

This example shows an RTSP camera, which is as simple as providing an URL. In this example, we use HTTP Basic Authentication (`username:password@...`) in the URL.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/drivers/my-rtsp-camera/device.mjs" %}

```mjs
import Homey from 'homey';

export default class MyRTSPDevice extends Homey.Device {

    /*
     * To play an RTSP stream, you simply need to return the URL to the stream
     * from the video url listener. Some streams require authentication in
     * different formats. This example uses query parameters for authentication.
     */
    async onInit() {        
        try {
            const video = await this.homey.videos.createVideoRTSP({
                allowInvalidCertificates: true,
                demuxer: 'h265',
            });

            /*
             * The video url listener takes no arguments. It simply builds the
             * URL to the RTSP stream using the username and password.
             */
            video.registerVideoUrlListener(async () => {
                // Get the username and password that were set during pairing
                const {
                    username,
                    password,
                 } = this.getSettings();
         
                // Normally, you would get the device's IP from Discovery, or another method
                return {
                    url: `rtsp://${username}:${password}@192.168.1.100:554/stream`
                };
            });

            /*
             * Attach the camera to the device.
             */
            await this.setCameraVideo('main', 'Main Camera', video);
        } catch (err) {
            this.error('Error creating camera:', err);
        }    
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/drivers/my-rtsp-camera/device.mts" %}

```mts
import Homey from "homey";

export default class Device extends Homey.Device {
  /*
   * To play an RTSP stream, you simply need to return the URL to the stream
   * from the video url listener. Some streams require authentication in
   * different formats. This example uses query parameters for authentication.
   */
  async onInit(): Promise<void> {
    try {
      const video = await this.homey.videos.createVideoRTSP({
        allowInvalidCertificates: true,
        demuxer: "h265",
      });

      /*
       * The video url listener takes no arguments. It simply builds the
       * URL to the RTSP stream using the username and password.
       */
      video.registerVideoUrlListener(async () => {
        // Get the username and password that were set during pairing
        const { username, password } = this.getSettings();

        // Normally, you would get the device's IP from Discovery, or another method
        return {
          url: `rtsp://${username}:${password}@192.168.1.100:554/stream`,
        };
      });

      /*
       * Attach the camera to the device.
       */
      await this.setCameraVideo("main", "Main Camera", video);
    } catch (err) {
      this.error("Error creating camera:", err);
    }
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/drivers/my-rtsp-camera/device.py" %}

```python
from homey import device


class Device(device.Device):
    # To play an RTSP stream, you simply need to return the URL to the stream
    # from the video url listener. Some streams require authentication in
    # different formats. This example uses query parameters for authentication.
    async def on_init(self) -> None:
        try:
            video = await self.homey.videos.create_video_rtsp(
                allow_invalid_certificates=True, demuxer="h265"
            )

            # The video url listener takes no arguments. It simply builds the
            # URL to the RTSP stream using the username and password.
            async def url_listener() -> str:
                # Get the username and password that were set during pairing
                settings = self.get_settings()
                username, password = settings.get("username"), settings.get("password")

                # Normally, you would get the device's IP from Discovery, or another method
                return f"rtsp://{username}:{password}@192.168.1.100:554/stream"

            video.register_video_url_listener(url_listener)

            # Attach the camera to the device.
            await self.set_camera_video("main", "Main Camera", video)
        except Exception as err:
            self.error("Error creating camera:", err)


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

### Example — RTMP

This example shows an RTMP camera, which is as simple as providing an URL. It's very similar to RTSP.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/drivers/my-rtmp-camera/device.mjs" %}

```mjs
import Homey from 'homey';

export default class MyRTMPDevice extends Homey.Device {

    async onInit() {        
        try {
            const video = await this.homey.videos.createVideoRTMP();

            /*
             * The video url listener takes no arguments. It simply builds the
             * URL to the RTMP stream.
             */
            video.registerVideoUrlListener(async () => {         
                // Normally, you would get the device's IP from Discovery, or another method
                return {
                    url: `rtmp://@192.168.1.100:1935`
                };
            });

            /*
             * Attach the camera to the device.
             */
            await this.setCameraVideo('main', 'Main Camera', video);
        } catch (err) {
            this.error('Error creating camera:', err);
        }    
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/drivers/my-rtmp-camera/device.mts" %}

```mts
import Homey from "homey";

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    try {
      const video = await this.homey.videos.createVideoRTMP();

      /*
       * The video url listener takes no arguments. It simply builds the
       * URL to the RTMP stream.
       */
      video.registerVideoUrlListener(async () => {
        // Normally, you would get the device's IP from Discovery, or another method
        return {
          url: `rtmp://@192.168.1.100:1935`,
        };
      });

      /*
       * Attach the camera to the device.
       */
      await this.setCameraVideo("main", "Main Camera", video);
    } catch (err) {
      this.error("Error creating camera:", err);
    }
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/drivers/my-rtmp-camera/device.py" %}

```python
from homey import device


class Device(device.Device):
    async def on_init(self) -> None:
        try:
            video = await self.homey.videos.create_video_rtmp()

            # The video url listener takes no arguments. It simply builds the
            # URL to the RTMP stream.
            async def url_listener() -> str:
                # Normally, you would get the device's IP from Discovery, or another method
                return f"rtmp://@192.168.1.100:1935"

            video.register_video_url_listener(url_listener)

            # Attach the camera to the device.
            await self.set_camera_video("main", "Main Camera", video)
        except Exception as err:
            self.error("Error creating camera:", err)


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

### Example — HLS

This example shows an HLS camera, which is as simple as providing an URL. It's very similar to RTSP.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/drivers/my-hls-camera/device.mjs" %}

```mjs
import Homey from 'homey';

export default class MyHLSDevice extends Homey.Device {

    async onInit() {        
        try {
            const video = await this.homey.videos.createVideoHLS();

            /*
             * The video url listener takes no arguments. It simply builds the
             * URL to the HLS stream.
             */
            video.registerVideoUrlListener(async () => {         
                // Normally, you would get the device's IP from Discovery, or another method
                return {
                    url: `http://@192.168.1.100/stream.m3u8`
                };
            });

            /*
             * Attach the camera to the device.
             */
            await this.setCameraVideo('main', 'Main Camera', video);
        } catch (err) {
            this.error('Error creating camera:', err);
        }    
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/drivers/my-hls-camera/device.mts" %}

```mts
import Homey from "homey";

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    try {
      const video = await this.homey.videos.createVideoHLS();

      /*
       * The video url listener takes no arguments. It simply builds the
       * URL to the HLS stream.
       */
      video.registerVideoUrlListener(async () => {
        // Normally, you would get the device's IP from Discovery, or another method
        return {
          url: `http://@192.168.1.100/stream.m3u8`,
        };
      });

      /*
       * Attach the camera to the device.
       */
      await this.setCameraVideo("main", "Main Camera", video);
    } catch (err) {
      this.error("Error creating camera:", err);
    }
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/drivers/my-hls-camera/device.py" %}

```python
from homey import device


class Device(device.Device):
    async def on_init(self) -> None:
        try:
            video = await self.homey.videos.create_video_hls()

            # The video url listener takes no arguments. It simply builds the
            # URL to the HLS stream.
            async def url_listener() -> str:
                # Normally, you would get the device's IP from Discovery, or another method
                return f"http://@192.168.1.100/stream.m3u8"

            video.register_video_url_listener(url_listener)

            # Attach the camera to the device.
            await self.set_camera_video("main", "Main Camera", video)
        except Exception as err:
            self.error("Error creating camera:", err)


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

### Example — DASH

This example shows an DASH camera, which is as simple as providing an URL. It's very similar to RTSP.

{% tabs %}
{% tab title="JavaScript" %}
{% code title="/drivers/my-dash-camera/device.mjs" %}

```mjs
import Homey from 'homey';

export default class MyDASHDevice extends Homey.Device {

    async onInit() {        
        try {
            const video = await this.homey.videos.createVideoDASH();

            /*
             * The video url listener takes no arguments. It simply builds the
             * URL to the DASH stream.
             */
            video.registerVideoUrlListener(async () => {         
                // Normally, you would get the device's IP from Discovery, or another method
                return {
                    url: `http://@192.168.1.100/stream.mpd`
                };
            });

            /*
             * Attach the camera to the device.
             */
            await this.setCameraVideo('main', 'Main Camera', video);
        } catch (err) {
            this.error('Error creating camera:', err);
        }    
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
{% code title="/drivers/my-dash-camera/device.mts" %}

```mts
import Homey from "homey";

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    try {
      const video = await this.homey.videos.createVideoDASH();

      /*
       * The video url listener takes no arguments. It simply builds the
       * URL to the DASH stream.
       */
      video.registerVideoUrlListener(async () => {
        // Normally, you would get the device's IP from Discovery, or another method
        return {
          url: `http://@192.168.1.100/stream.mpd`,
        };
      });

      /*
       * Attach the camera to the device.
       */
      await this.setCameraVideo("main", "Main Camera", video);
    } catch (err) {
      this.error("Error creating camera:", err);
    }
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
{% code title="/drivers/my-dash-camera/device.py" %}

```python
from homey import device


class Device(device.Device):
    async def on_init(self) -> None:
        try:
            video = await self.homey.videos.create_video_dash()

            # The video url listener takes no arguments. It simply builds the
            # URL to the DASH stream.
            async def url_listener() -> str:
                # Normally, you would get the device's IP from Discovery, or another method
                return f"http://@192.168.1.100/stream.mpd"

            video.register_video_url_listener(url_listener)

            # Attach the camera to the device.
            await self.set_camera_video("main", "Main Camera", video)
        except Exception as err:
            self.error("Error creating camera:", err)


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

## Apps SDK Reference

{% tabs %}
{% tab title="JavaScript" %}
Please refer to [ManagerVideos](https://apps-sdk-v3.developer.homey.app/ManagerVideos.html) in the Apps SDK Reference to learn more about videos in your app.
{% endtab %}

{% tab title="TypeScript" %}
Please refer to [ManagerVideos](https://apps-sdk-v3.developer.homey.app/ManagerVideos.html) in the Apps SDK Reference to learn more about videos in your app.
{% endtab %}

{% tab title="Python" %}
Please refer to [ManagerVideos](https://python-apps-sdk-v3.developer.homey.app/manager/videos.html#homey.manager.videos.ManagerVideos) in the Apps SDK Reference to learn more about videos in your app.
{% endtab %}
{% endtabs %}


# LED Ring

Homey's LED ring consists of 24 RGB LED's that apps can control and supply new animations for.

{% hint style="warning" %}
The LED Ring can only be controlled on Homey Pro (Early 2019) and older models. Add `platformLocalRequiredFeatures` to the [App Manifest](https://apps.developer.homey.app/the-basics/app/manifest#platform-local-required-features) to make sure the app can not be installed on Homey Pros that do not have a controllable LED Ring.
{% endhint %}

Your app may control Homey's LED Ring to give visual feedback when your app is doing something. It is also possible to take control of the LED Ring when Homey is idling, this is called a [screensaver](#screensaver).

{% hint style="info" %}
In order to access the LED Ring your app will need the `homey:manager:ledring` permission. For more information about permissions read the [Permissions guide](/the-basics/app/permissions).
{% endhint %}

## Playing a LED Ring animation

To keep the user interaction of Homey consistent, you can play a system-provided animation.

{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  async onInit() {
    const pulseAnimation = await this.homey.ledring.createSystemAnimation("pulse");
    await pulseAnimation.start();
  }
}

module.exports = App;
```

{% endcode %}

## Creating your own LED Ring animation

An animation is essentially a JavaScript object with frames and settings about the frame speed and rotation speed.

{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  async onInit() {
    const myAnimation = await this.homey.ledring.createAnimation({
      options: {
        fps: 1, // real frames per second
        tfps: 60, // target frames per second. this means that every frame will be interpolated 60 times
        rpm: 0, // rotations per minute
      },
      frames: [],
      priority: "INFORMATIVE", // or FEEDBACK, or CRITICAL
      duration: 3000, // duration in ms, or keep empty for infinite
    });

    // register the animation with Homey
    myAnimation
      .on("start", () => {
        // The animation has started playing
      })
      .on("stop", () => {
        // The animation has stopped playing
      });

    await myAnimation.start();
  }
}

module.exports = App;
```

{% endcode %}

The animation object is an `Array`, which contains frames. A frame is an `Array` with 24 `Object` values, that represent the color of that pixel. The pixel object is contains the properties `r`, `g` and `b`, respectively *red*, *green* and *blue* on a scale of `0 - 255`.

For example, to create a spinning red dot:

```javascript
const frames = [];
const frame = [];

// for every pixel...
for (let pixelIndex = 0; pixelIndex < 24; pixelIndex++) {
  let colors = {
    r: 0,
    g: 0,
    b: 0,
  };

  // set the first pixel to red
  if (pixelIndex === 0) {
    colors.r = 255;
  }

  frame.push(colors);
}

// and finally, add the generated frame to the frames array
frames.push(frame);
```

## Screensaver

Your app can register a screensaver, which can be chosen by going to *Settings → LED Ring*, and executed when Homey is idling.

Define your screensaver(s) in your App Manifest as follows:

{% code title="/.homeycompose/screensavers/weather.json" %}

```javascript
{
  "title": {
    "en": "Weather",
    "nl": "Weer"
  }
}
```

{% endcode %}

And from within your app, register your animation instance as follows:

{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  async onInit() {
    const myAnimation = await this.homey.ledring.createAnimation({
      // ...
    });

    // If this animation is also a screensaver, we must register it first.
    // 'weather' is the screensaver name defined in our app.json
    myAnimation.registerScreensaver("weather")
      .then(this.log)
      .catch(this.error);
  }
}

module.exports = App;
```

{% endcode %}

For live animations, simply update your animation using [`LedringAnimation#updateFrames()`](https://apps-sdk-v3.developer.homey.app/LedringAnimation.html#updateFrames) when needed.


# Homey Compose

Homey Compose makes developing Homey Apps easier by splitting up the App Manifest into multiple smaller manifests.

## File structure

The file structure when using compose looks as follows. This is an overview of all files that can be used with Homey Compose.

```
com.athom.example/
├─ .homeycompose/
│  ├─ app.json
│  ├─ capabilities/
│  │  └─ <id>.json
│  ├─ screensavers/
│  │  └─ <id>.json
│  ├─ signals/
│  │  ├─ 433/
│  │  │  └─ <id>.json
│  │  ├─ 868/
│  │  │  └─ <id>.json
│  │  └─ ir/
│  │     └─ <id>.json
│  ├─ flow/
│  │  ├─ triggers/
│  │  │  └─ <id>.json
│  │  ├─ conditions/
│  │  │  └─ <id>.json
│  │  └─ actions/
│  │     └─ <id>.json
│  ├─ discovery/
│  │  └─ <id>.json
│  ├─ drivers/
│  │  ├─ templates/
│  │  │  └─ <template_id>.json
│  │  ├─ settings/
│  │  │  └─ <setting_id>.json
│  │  └─ flow/
│  │     ├─ triggers/
│  │     │  └─ <id>.json
│  │     ├─ conditions/
│  │     │  └─ <id>.json
│  │     └─ actions/
│  │        └─ <id>.json
│  └─ locales/
│     ├─ <locale>.json
│     └─ <locale.foo>.json
└─ drivers/
   └─ <driver_id>/
      ├─ driver.compose.json
      ├─ driver.flow.compose.json
      └─ driver.settings.compose.json
```

## Templating

Compose also allows for creating driver and setting templates to share common properties, for example RF signals, assets, etc. You can create a driver template by placing the shared properties in `.homeycompose/drivers/templates/<template_id>.json`. Then extend from the template in your driver compose file by placing the template id the `$extends` property in `driver.compose.json`. Compose will copy all properties from the template to the final file. Should you want to overwrite a property, you can do so by placing it in your `driver.compose.json`.

{% code title="/.homeycompose/drivers/templates/defaults.json" %}

```javascript
{
  "images": {
    "large": "{{driverAssetsPath}}/images/large.png",
    "small": "{{driverAssetsPath}}/images/small.png"
  },
  "icon": "{{driverAssetsPath}}/icon.svg",
  "capabilities": [],
  "class": "other"
}
```

{% endcode %}

{% code title="/drivers/my\_driver/driver.compose.json" %}

```javascript
{
  "name": {
    "en": "My Driver",
    "nl": "Mijn Driver"
  },
  "$extends": ["defaults"]
}
```

{% endcode %}

By using this template, you only have to place the values that are uniqiue to a driver in the `driver.compose.json` file. All other required properties are copied over from the template.


# Homey Cloud

Everything you need to know to make your apps compatible with Homey Cloud.

The Homey Apps SDK is engineered to support both Homey Pro and Homey Cloud. Whether you developed your app for Homey Cloud or Homey Pro, it likely already works on both platforms. However there are some important differences that you should be aware of. This page will explain those differences and help you make sure your app offers the best experience to users whether they use Homey Cloud or Homey Pro.

![](/files/-Mj-77CUnjjO1rSqXHbF)

## Architecture

It is important to know how your apps will be run in Homey Cloud. On Homey Cloud your app is responsible for more than one user at a time. We refer to this as multi-tenancy, which means that several app instances are created inside the same Node.js process. In addition if your app has many users it will get started on several servers to spread the load.

![](/files/-Mj-cCcRy-bzPMs1HdSg)

Despite Homey Cloud running apps in the cloud it is still possibly to easily test your app during development. If you run `homey app run` while having a Cloud Homey selected, the Homey CLI will automatically guide you to install all the required tools. It will then run the app on your computer and connect it to Homey Cloud.

## Verified apps

Because Homey Cloud is offered for a mainstream audience, we want to ensure every app is of great quality. To assist with this, we offer code-level support and in-depth app reviews as part of the [Homey Verified Developer](https://homey.app/homey-verified-developer/) subscription. This subscription is required to publish your app to the App Store when targeting `"platforms": [ "cloud" ]` or `"platforms": [ "local", "cloud" ]`.

Only official app integrations will be approved. This means an app submitted by a brand or submitted by a third party developer commissioned by said brand.

{% hint style="info" %}
Publishing apps for Homey Pro does not require a subscription and will always remain free.
{% endhint %}

You can check whether your app passes some of the additional validation by running:

```
homey app validate --level verified
```

## Adding supported platforms

To indicate that your app supports running on Homey Cloud you will need to add the `platforms` flag to your App, Driver and Flow manifests.

{% code title="/.homeycompose/app.json" %}

```javascript
{
  "id": "my.company.example",
  "version": "1.0.0",
  "compatibility": ">=5.0.0",
  "platforms": ["local", "cloud"],
  "sdk": 3,
  // ...
}
```

{% endcode %}

{% hint style="info" %}
Only SDK v3 is supported on Homey Cloud so before you start making your app compatible with Homey Cloud, make sure you are using SDK v3.
{% endhint %}

{% code title="/.homeycompose/flow/triggers/rain\_start.json" %}

```javascript
{
  "title": {
    "en": "It starts raining"
  },
  "hint": {
    "en": "When it starts raining more than 0.1 mm/h."
  }
  "platforms": ["local", "cloud"]
}
```

{% endcode %}

Each individual driver can list the supported platforms and the wireless connectivity that is used with the `platforms` and `connectivity` properties:

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "platforms": ["local", "cloud"],
  "connectivity": ["ble"],
  "class": "light",
  "capabilities": ["onoff", "dim"],
}
```

{% endcode %}

The available options for `connectivity` are:

| Value      | Description                                                                                                                        |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `lan`      | This is not possible with Homey Bridge, see [SDK difference between Homey Pro and Homey Cloud](#local-wi-fi-and-device-discovery). |
| `cloud`    | This means that your Driver uses OAuth or Webhooks to connect to a cloud service.                                                  |
| `ble`      | Use if your Driver connects to Bluetooth Low Energy devices.                                                                       |
| `zwave`    | Use if your Driver implements a Z-Wave device.                                                                                     |
| `zigbee`   | Use if your Driver implements a Zigbee device.                                                                                     |
| `infrared` | Use if your Driver sends infrared signals.                                                                                         |
| `rf433`    | Use if your Driver sends 433Mhz signals.                                                                                           |
| `rf868`    | This is not possible with Homey Bridge.                                                                                            |

Read the[ App Store Guidelines](/app-store/guidelines) for more information.

## Mutating global variables

In order to handle multi-tenancy for Homey Cloud, your app should not use global variables. Because the global scope is shared the values of global variables may be unpredictable.

This is a contrived example of what **you should not** do:

{% code title="/app.js" %}

```javascript
const Homey = require('homey');

// BAD: This variable will be shared by several app instances
let count = 0; 

class App extends Homey.App {
  onInit() {
    count += 1;
  }
}

module.exports = App;
```

{% endcode %}

Instead **you should** set properties on your `App`, `Driver` or `Device` instances:

{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  onInit() {
    // GOOD: this variable is only used by a single app instance
    this.count += 1;
  }
}

module.exports = App;
```

{% endcode %}

## Cleanup

Multi-tenancy also means that your app should cleanup when an instance is destroyed. For example if a user removes an app. On Homey Pro that causes the entire app process to be killed but on Homey Cloud you need to put in some extra care to make sure all your resources are correctly released.

### App lifecycle

In order to make this easier the Apps SDK as several "un-init" methods: `App#onUninit()`, `Driver#onUninit()`, and `Device#onUninit()`. These methods will be called when your app is destroyed so you can perform cleanup in them. For example:

{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  async onInit() {
    this.api = new DeviceApi();
  }
  
  async onUninit() {
    this.api.destroy();
  }
}

module.exports = App;
```

{% endcode %}

### Clearing Timers

Another case in which you need to take care to clean up when your app is uninstalled would be timers like `setInterval()` and `setTimeout()`. You should not call these directly, instead you should use `this.homey.setInterval()` or `this.homey.setTimeout()`. These act exactly the same as their regular counterparts but will automatically be cleared when your app is destroyed.

**Make sure you don't do this:**

{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  onInit() {
    // BAD: the interval is not cleaned up after the app instance is destroyed
    setInterval(() => {
      // do something
    }, 10000);
  }
}

module.exports = App;
```

{% endcode %}

**Instead write code like this:**

{% code title="/app.js" %}

```javascript
const Homey = require('homey');

class App extends Homey.App {
  onInit() {
    // GOOD: this will automatically be cleared when the app is destroyed
    this.homey.setInterval(() => {
      // do something
    }, 10000);
  }
}

module.exports = App;
```

{% endcode %}

## Unhandled Promise rejections

On Homey Cloud unhandled promise rejections will cause the app to crash. Unhandled promise rejections can cause memory leaks and may indicate that your app is not correctly handling errors. If errors do not matter you can, in most cases, simply log the error using: `.catch(this.error);`

{% hint style="info" %}
This behaviour is about to be standard in Node.js and is therefore also coming to Homey Pro in the near future.
{% endhint %}

An example of this is `Device#setCapabilityValue()` which returns a promise. In order to prevent unhandled promise rejections you should add `.catch(this.error)`.

**Make sure you don't do this:**

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require('homey');

class Device extends Homey.Device {
  async onInit() {
    // BAD: this returns a promise, which may cause an unhandled promise rejection
    this.setCapabilityValue('onoff', false);
  }
}

module.exports = Device;
```

{% endcode %}

**Instead write code like this:**

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require('homey');

class Device extends Homey.Device {
  async onInit() {
    // GOOD: catching the promise prevents unhandled promise rejections
    this.setCapabilityValue('onoff', false).catch(this.error);
  }
}

module.exports = Device;
```

{% endcode %}

## SDK differences between Homey Pro & Homey Cloud

Homey Cloud apps are not allowed to do everything that apps on Homey Pro are allowed to do. The following Apps SDK features are not supported on Homey Cloud:

### App Web API

Homey Cloud does not have support for [App Web APIs](/advanced/web-api). While this means that you cannot expose a complete REST API you can still receive Webhook updates. Read the [Webhooks](/cloud/webhooks) documentation to learn more.

### App-to-app communication

App-to-app communication is usually pretty tricky for a user to setup and may break due to app changes. Because of this it is not supported on Homey Cloud. This means that apps that run on Homey Cloud cannot have `homey:app:<appId>` permissions. Likewise the `homey:manager:api` permission that allowed an app to use [ManagerApi](https://apps-sdk-v3.developer.homey.app/ManagerApi.html) and the Homey Web API is not allowed.

### Local Wi-Fi & Device Discovery

Homey Bridge does not support local Wi-Fi connections, therefore mDNS, SSDP and MAC discovery are not supported.

Obviously this means that [`ManagerCloud#getLocalAddress()`](https://apps-sdk-v3.developer.homey.app/ManagerCloud.html#getLocalAddress) is also not supported on Homey Cloud.

### App Settings

To simplify the user experience, custom app settings views are not supported on Homey Cloud.

Any information your app might need to function should be asked for when pairing a device. If information needs to be updated you can use the ["re-pair" pairing views](/the-basics/devices/pairing#repairing) so a users Flows don't become broken. Read the [Custom Pairing Views](/advanced/custom-views/custom-pairing-views) documentation for more information.

### Relative vs. Absolute Paths

On Homey Pro, apps are sandboxed and chroot'ed. On Homey Cloud, apps are run in a Docker container.

This means that the root-path `/` is different. On Homey Pro, `/` is your app's directory, and on Homey Cloud, `/` is the Linux root.

To ensure your app works on both platforms, always require files relatively, e.g. `require('./assets/foo.js')` or `path.join(__dirname, 'myfile.svg')`.

## Avoiding app review rejections for Homey Cloud

Because Homey Cloud is targeted at a broader audience not all the types of apps that are available on Homey Pro are going to be approved for Homey Cloud.

Apps that add Drivers for a brand of smart home devices are very welcome on Homey Cloud. However apps that add advanced functionality that can only be used in combination with other apps are going to be rejected.

Generally these kinds of apps are found in the "Tools" category on the Homey Apps Store. Most of these apps won't work on Homey Cloud because the `homey:manager:api` permission is not allowed to be used.

If you are a verified developer and are questioning whether your app idea will be allowed to be published on Homey Cloud be sure to get in touch with the developer support.


# Breaking Changes

This guide gives hints and guidance in the case that you need to make a change to your app that might break functionality for current users.

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 need to 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!

## Adding capabilities

{% tabs %}
{% tab title="JavaScript" %}
You can add new capabilities to your devices simply by adding them to the `/drivers/<driver_id>/driver.compose.json`. However devices that have already been paired do not automatically receive the new capability. To prevent users of your app from having to re-pair their devices to get the new functionality you can call [`Device#addCapability()`](https://apps-sdk-v3.developer.homey.app/Device.html#addCapability) to dynamically add the capability to already-paired devices.

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require('homey');

class Device extends Homey.Device {
  async onInit() {
    if (this.hasCapability('windowcoverings_set') === false) {
      // You need to check if migration is needed
      // do not call addCapability on every init!
      await this.addCapability('windowcoverings_set');
    }
  }
}

module.exports = Device;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
You can add new capabilities to your devices simply by adding them to the `/drivers/<driver_id>/driver.compose.json`. However devices that have already been paired do not automatically receive the new capability. To prevent users of your app from having to re-pair their devices to get the new functionality you can call [`Device#addCapability()`](https://apps-sdk-v3.developer.homey.app/Device.html#addCapability) to dynamically add the capability to already-paired devices.

{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey from "homey";

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    if (!this.hasCapability("windowcoverings_set")) {
      // You need to check if migration is needed
      // Do not call addCapability on every init!
      await this.addCapability("windowcoverings_set");
    }
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
You can add new capabilities to your devices simply by adding them to the `/drivers/<driver_id>/driver.compose.json`. However devices that have already been paired do not automatically receive the new capability. To prevent users of your app from having to re-pair their devices to get the new functionality you can call [`Device#add_capability()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.add_capability) to dynamically add the capability to already-paired devices.

{% code title="/drivers/\<driver\_id>/device.py" %}

```python
from homey import device


class Device(device.Device):
    async def on_init(self) -> None:
        if not self.has_capability("windowcoverings_set"):
            # You need to check if migration is needed
            # Do not call add_capability on every init!
            await self.add_capability("windowcoverings_set")


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

## Removing capabilities

While maintaining your app you may encounter a situation where you want to remove a capability that is no longer useful.

In most cases, it is best to only remove the capability from the driver object in the App Manifest. This makes sure that for already paired devices nothing will break, UI components and Flow cards will remain working. However, the removed capability will not be added to newly paired devices. 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.

{% tabs %}
{% tab title="JavaScript" %}
It is also possible to remove the capability from devices that have already been paired by calling [`Device#removeCapability()`](https://apps-sdk-v3.developer.homey.app/Device.html#removeCapability). You should only do this when it is no longer possible to implement the capabilities behaviour.

{% hint style="danger" %}
If the capability you are removing has Flow cards, those cards will also be removed. Flows using these Flow cards will break.
{% endhint %}

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require('homey')

class Device extends Homey.Device {
  async onInit() {
    if (this.hasCapability('windowcoverings_state')) {
      // You need to check if migration is needed
      // do not call removeCapability on every init!
      await this.removeCapability('windowcoverings_state');
    }
  }
}

module.exports = Device;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
It is also possible to remove the capability from devices that have already been paired by calling [`Device#removeCapability()`](https://apps-sdk-v3.developer.homey.app/Device.html#removeCapability). You should only do this when it is no longer possible to implement the capabilities behaviour.

{% hint style="danger" %}
If the capability you are removing has Flow cards, those cards will also be removed. Flows using these Flow cards will break.
{% endhint %}

{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey from "homey";

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    if (this.hasCapability("windowcoverings_state")) {
      // You need to check if migration is needed
      // Do not call removeCapability on every init!
      await this.removeCapability("windowcoverings_state");
    }
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
It is also possible to remove the capability from devices that have already been paired by calling [`Device#remove_capability()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.remove_capability). You should only do this when it is no longer possible to implement the capabilities behaviour.

{% hint style="danger" %}
If the capability you are removing has Flow cards, those cards will also be removed. Flows using these Flow cards will break.
{% endhint %}

{% code title="/drivers/\<driver\_id>/device.py" %}

```python
from homey import device


class Device(device.Device):
    async def on_init(self) -> None:
        if self.has_capability("windowcoverings_state"):
            # You need to check if migration is needed
            # Do not call remove_capability on every init!
            await self.remove_capability("windowcoverings_state")


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

## 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.

{% hint style="warning" %}
Do not remove or change the Flow card run listener in your code, this would still break existing Flows.
{% endhint %}

{% code title="/.homeycompose/flow/actions/stop\_raining.json" %}

```javascript
{
  "title": { "en": "Flow Action Title" },
  "deprecated": true
}
```

{% endcode %}

## Changing the device class

{% tabs %}
{% tab title="JavaScript" %}
If you need to change the device class of your device, the [`Device#setClass()`](https://apps-sdk-v3.developer.homey.app/Device.html#setClass) method is available to do so.

{% hint style="warning" %}
Some Flow cards might depend on a certain device class, changing the device's device class will result in broken Flows for users in that case.
{% endhint %}

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const Homey = require('homey');

class Device extends Homey.Device {
  async onInit() {
    if (this.getClass() !== 'light') {
      // You need to check if migration is needed
      // do not call setClass on every init!
      await this.setClass('light').catch(this.error)
    }
  }
}

module.exports = Device;
```

{% endcode %}
{% endtab %}

{% tab title="TypeScript" %}
If you need to change the device class of your device, the [`Device#setClass()`](https://apps-sdk-v3.developer.homey.app/Device.html#setClass) method is available to do so.

{% hint style="warning" %}
Some Flow cards might depend on a certain device class, changing the device's device class will result in broken Flows for users in that case.
{% endhint %}

{% code title="/drivers/\<driver\_id>/device.mts" %}

```mts
import Homey from "homey";

export default class Device extends Homey.Device {
  async onInit(): Promise<void> {
    if (this.getClass() !== "light") {
      // You need to check if migration is needed
      // Do not call setClass on every init!
      await this.setClass("light").catch(this.error);
    }
  }
}

```

{% endcode %}
{% endtab %}

{% tab title="Python" %}
If you need to change the device class of your device, the [`Device#set_class()`](https://python-apps-sdk-v3.developer.homey.app/device.html#homey.device.Device.set_class) method is available to do so.

{% hint style="warning" %}
Some Flow cards might depend on a certain device class, changing the device's device class will result in broken Flows for users in that case.
{% endhint %}

{% code title="/drivers/\<driver\_id>/device.py" %}

```python
from homey import device


class Device(device.Device):
    async def on_init(self) -> None:
        if self.get_class() != "light":
            # You need to check if migration is needed
            # Do not call set_class on every init!
            try:
                await self.set_class("light")
            except Exception as err:
                self.error(err)


homey_export = Device

```

{% endcode %}
{% endtab %}
{% endtabs %}

## 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. Deprecating a driver will ensure that devices that have already been paired will continue to function but the driver cannot be selected to pair new devices with.

{% code title="/drivers/\<driver\_id>/driver.compose.json" %}

```javascript
{
  "name": { "en": "My Driver" },
  "deprecated": true,
  "capabilities": ["onoff", "dim"]
}
```

{% endcode %}


# Tools

In this section we have some recommendations for tools that can make your development easier. Some of these are specific to Homey but others help you develop high quality JavaScript applications.

If you are developing a Homey App using Bluetooth LE, the [Bluetooth LE Devtools](https://tools.developer.homey.app/tools/ble) may come in handy. With these tools you get a quick but powerful overview of all Bluetooth LE devices near homey and you can easily gather all the necessary information to connect to and control your devices.

{% content-ref url="/pages/-MYPZUPzbxOW-VVyh7ha" %}
[Bluetooth LE](/guides/tools/bluetooth)
{% endcontent-ref %}

If you are developing a Zigbee app for Homey, the [Zigbee Devtools](https://tools.developer.homey.app/tools/zigbee) give you a quick overview of the Zigbee network and some basic system information. This can be used to quickly troubleshoot your devices and Homey Apps.

{% content-ref url="/pages/-MYAMYirNAf8scG6AfNM" %}
[Zigbee](/guides/tools/zigbee)
{% endcontent-ref %}

The Homey CLI has special support for TypeScript, not only does this give you a great autocomplete experience while developing your app. It also allows you to catch errors before you run your code. Read the TypeScript guide to learn how to setup your app to use TypeScript.

{% content-ref url="/pages/-MYPMXjmPTSbWkYWe-BW" %}
[TypeScript](/guides/tools/typescript)
{% endcontent-ref %}


# Bluetooth LE

We have created the BLE Developer Tool to support you in the creation of Bluetooth Low Energy apps. This tool makes it easy to explore the functionality of a device without having to write code.

## Overview

You can find the [BLE Developer Tool](https://tools.developer.homey.app/tools/ble) in the [Homey Developer Portal](https://tools.developer.homey.app/).

With this tool you can discover, make connection, and communicate with these devices through Homey. The tool follows the structure of the BLE hierarchy, which is as follows: *All Advertisements -> Peripheral -> Service -> Characteristic -> Descriptor*. Below each level of the hierarchy is described in more detail.

## Advertisements

Advertisements shows all devices detected by Homey sorted on signal strength (`RSSI`). It is possible to perform a device discovery by clicking the 'discover devices' button at the top of the column. Clicking on one of the advertisements will open the peripheral, which provides more details on that device.

![Column 1: Advertisements](/files/-MYPaE1WHddeDqwvTZ06)

## Peripheral

The peripheral shows more information for the selected device. For most BLE devices it is possible to connect and disconnect with the device.

{% hint style="info" %}
Some devices cannot be connected to, for example when all their data is shown in the advertisement already and a connection is not necessary.
{% endhint %}

To interact with a device, first a connection needs to be made. After a succesfull connection more options become available, such as discovering Services and Characteristics, updating RSSI and disconnection from the device. Selecting a service will show more details and functions that can be performed on the selected service.

{% hint style="info" %}
The *"Discover Services"* and "*Discover Services & Characteristics"* initially perform the same operation, although the latter one will save you some time in the next column, which is the service column.
{% endhint %}

![Column 2: Peripheral](/files/-MYPbb6U1MZvR-nZOEGw)

## Services

Every service is a collection of one ore more Characteristics. These need to be discovered first before more information can be displayed.

![](/files/-MYPdVKBzC2SqpCBX7jd)

## Characteristics

Characteristics are the most complex section in the BLE Developer Tool. Each characteristic is a specific functionality of the device. This can range from reading the device identifier to telling the device to perform a specific action (for instance a BLE light bulb can be told to change it's color).

### Reading and Writing

Most characterisics can be used to read and/or write to the device. Data read from a device will be shown in multiple formats, helping you to quickly decode the data.

{% hint style="info" %}
Often a characteristic that can be written to will also allow you to read the current value of that descriptor. If you have a RGBW lamp it may be possible to read from a characteristic and receive the result \[0, 255, 0, 0]. If your lamp is currently green, the data is probably formatted like \[R, G, B, W]. If your lamp is red, it is probably formatted like \[W, R, G, B]. In such a case you can try writing \[255, 0, 0, 0] to see what it does.
{% endhint %}

![Reading, Writing and Descriptor discovery.](/files/-MYPgA_edYUqB3chDjLV)

![Read results from a characteristic user description descriptor](/files/-MYPgbR07BYhaPvRZCGc)

![Writing data. You need to write the data as a buffer in decimal format. (for example: \[255, 0, 0\])](/files/-MYPgUdi_IJNPxVuFr63)

![The Characteristic User Description Descriptor. Reading from this descriptor is often a good idea.](/files/-MYPgZn-eCHHkWzi-Vyq)

{% hint style="info" %}
Reading from a characteristic often will only give you some raw data without explanation or description. Often there is a "Characteristic User Description" - descriptor present that provides more details on the meaning of these values in the descriptor.
{% endhint %}

### BLE Notifications

The BLE Developer tool allows you to subscribe for BLE Notifications. After subscribing, you can view all the received notifications from the device.

![The buttons for using BLE Notification functionality. If BLE Notifications are not supported by the device these buttons are disabled.](/files/-MYPejiXYjui-FQWyLpg)

![A live feed of incoming notifications.](/files/-MYPff2cieVctgiWlrfA)

## Descriptors

Although not always present, descriptors provide extra information about the characteristic it belongs to, for example a user description or subscription status. The "read" and "write" buttons behave similar to the "read" and "write" buttons in a characteristic.

![The characteristic view for the "Characteristic User Description" - Descriptor.](/files/-MYPjB0yKLttzuXIMQZN)


# Zigbee

To support developers in creating drivers for Zigbee devices the Zigbee Developer Tools have been created. This page will explain the functionality of the Zigbee Developer Tools.

## Overview

You can find the [Zigbee Developer Tools](https://tools.developer.homey.app/tools/zigbee) in the [Homey Developer Portal](https://tools.developer.homey.app/).

The tool consists of two main sections, the nodes table on top and the system information below. The nodes table presents information about the Zigbee network while the system information displays information about the current state of Homeys Zigbee chip.

## Nodes Table

![](/files/-MYANM9W0XUWvEfWGHgJ)

At the top of page the Nodes Table is displayed. This table shows all the Zigbee devices connected to Homey. Each devices has a number of properties:

* *Node ID*: A random number to identify the node in the table.
* *IEEE Address*: The unique identifier of a Zigbee device.
* *Network Address*: The current network address of the node on the network.
* *Type*: Router or EndDevice, the former is a mains-powered device that acts as repeater, the latter is a battery device that is mostly asleep.
* *Online*: This property is updated when clicking the refresh button in the top right and shows if the node is currently responding to a ping request.
* *Receive When Idle*: Battery devices are asleep most of the time and are therefore not able to receive commands at all times, only routers can receive when idle.
* *Manufacturer*: This is the `manufacturerName` you should include in your driver's manifest.
* *Model ID*: This is the `productId` you should include in your driver's manifest.
* *Route*: The last known route that was taken when communicating with the node over the network.

### Interview

The interview button in the Nodes Table can be used to get an overview of the endpoints, clusters, commands and attributes supported by a specific node. When interviewing the node must be online. The process might take a while, especially for EndDevices since they are slower to respond. When the interview is finished, the information will appear with a JSON structure of the node.This contains the `modelId` and `manufacturerName` which you should use in your driver's manifest. Second, the `endpointDescriptors` represent all endpoints on the node and the clusters that are supported by each endpoint. Last, the `endpoints` is a nested object with a lot more information on each cluster, such as supported commands, attributes and attribute reporting configurations.

### Refresh Nodes

The refresh nodes button in the top right corner of the Nodes Table can be used to send a "ping request" to all router nodes on the network. This will update the "Online" property of the node. Note: this might take a while and will put considerable load on the Zigbee network for a short period of time.

### Routes

The route that is shown is the last known route that was taken when communicating with the node. This is often not the “shortest path”, but this is inherent to the way messages are sent through the Zigbee networ&#x6B;**.** The Zigbee protocol handles how a message is sent from one node to the other and compares the signal strength in the process. The signal strength will vary largely due to many things, walls, objects in the environment, distance, but also the radio strength of the node. The table only shows the last route that was reported, this is not something Homey determines or controls.

## System Information

![](/files/-MYANM9Xvu2xmGNG6erM)

This section shows the main properties of the Zigbee network.

* *Channel*: The current channel Homey is operating on, this is also a selector that allows you to change the channel.
* *Pan ID*: A Personal Area Network ID which is an ID of the Zigbee network on this Homey.
* *Extended PAN ID*: An Extended Personal Area Network ID which is an ID of the Zigbee network on this Homey.
* *IEEE Address*: The unique identifier of Homey on the Zigbee network.
* *Network Key*: The network key that is used to encrypt all Zigbee traffic on Homey.
* *Network Address*: Since Homey is the coordinator, this value is always zero.
* *Current Command*: If Homey is busy with something (such as an extended interview) this will reflect that.

### Change Channel

The channel selector allows for changing the channel Homey is operating on. This a very radical change to the network. Some devices might not transfer to the new channel properly and will need to be repaired. To prevent this as much as possible it is required that all Zigbee routers are online at the moment the channel is changed. This will make sure they receive the message to change the channel. Changing the channel can take up to 10 minutes. Only use this functionality if you are sure it is what you want.


# TypeScript

TypeScript allows you to catch bugs in your JavaScript code without having to run your Homey Apps. The Homey CLI makes it easy to get started developing Homey Apps with TypeScript.

## TypeScript in the Homey CLI

[TypeScript](https://www.typescriptlang.org) enables you to add types to your code which may aid you when developing your app. The Homey CLI allows you to choose between JavaScript and TypeScript when creating a new app. It is also possible to convert an existing app written in Javascript to TypeScript. This can even be done on a file by file basis.

### How it works

TypeScript 'transpiles' to JavaScript. This means that the TypeScript you write will be converted to JavaScript that runs on Homey. When your app is being processed to run on a Homey, it is possible to invoke a TypeScript compiler as an extra build step. The compiler compiles your files to a folder called `.homeybuild/` in the root folder of your app. After this compilation, Homey will bundle all JavaScript files into one app.

#### Creating new drivers with TypeScript

When the file `tsconfig.json` is present in the root folder of your app, the Homey CLI will default to TypeScript when running `homey app driver create`. If you want to avoid this, remove or rename `tsconfig.json` file.

{% hint style="warning" %}
Removing or remaning the `tsconfig.json` file will also prevent the TypeScript compiler from being invoked when running/installing/publishing your app.
{% endhint %}

## Creating a new app using TypeScript

Run `homey app create` in your terminal and answer 'Yes' when the CLI asks you to initialize your app with TypeScript utilities. All necessary and recommended dependencies and files will be created for you.

## Converting an existing app to TypeScript

If you have already created your app using JavaScript, but you want to convert your app to TypeScript, then this can only be performed manually. However, the conversion to TypeScript can be done file-by-file using the following steps:

#### Create a .tsconfig.json

The first thing to do is adding a file named `.tsconfig.json` to the root folder of your App. This will make Homey recognize your app as a TypeScript app. You are free to configure `tsconfig.json` to your liking. Only exception being the `outDir` which should remain `.homeybuild/`and having `sourceMap: true` is strongly recommended.

{% code title="/tsconfig.json" %}

```javascript
{
  "extends": "@tsconfig/node12/tsconfig.json",
  "compilerOptions": {
    "outDir": ".homeybuild/",
    "sourceMap": true
  }
}
```

{% endcode %}

#### Install dependencies

Make sure you have the file `.tsconfig.json` present in the root folder of your app. Then run the following command to install all necessary dependencies.

```
homey app add-types
```

#### Change your App entrypoint:

Rename app.js to app.ts and add the following lines at the top (You can remove `'use strict'`).

{% code title="/app.ts" %}

```
import sourceMapSupport from 'source-map-support';
sourceMapSupport.install();
...
```

{% endcode %}

#### Add the build step

Add a TypeScript transpile step in the npm `build` script.

{% code title="/package.json" %}

```
  "scripts": {
    "build": "tsc"
  },
```

{% endcode %}

#### All set!

Now you're all set! Run your app with `homey app run`. If you performed all steps correctly, then you should see a `Compiling TypeScript...` prompt before your app is being run.


# Using ESM in Homey Apps

Since Homey v12.0.1 it's possible to create Homey apps using ECMAScript Modules (ESM). You can refer to the [Node.js ESM documentation](https://nodejs.org/docs/latest-v16.x/api/esm.html) for more details. ESM brings certain advantages such as improved module isolation and native support for asynchronous loading, making it a more robust choice for modern JavaScript applications.

## CommonJS vs. ESM

Before this release, Homey apps used CommonJS (CJS) for module management, requiring modules with `require()` and exporting them using `module.exports`. However, with ESM, modules are imported and exported using `import` and `export` statements.

#### CJS Example:

```javascript
'use strict';

const Homey = require('homey');

class MyApp extends Homey.App {
  async onInit() {
    this.log('MyApp has been initialized');
  }
}

module.exports = MyApp;
```

#### ESM Example:

```javascript
import Homey from 'homey';

class MyApp extends Homey.App {
  async onInit() {
    this.log('MyApp has been initialized');
  }
}

export default MyApp;

```

## Using `.mjs`

Right now there is only one way to opt into using ESM in your Homey app:

1. **Using the `.mjs` extension:** Rename all your JavaScript files with the `.mjs` extension. This tells Node.js that these files should be treated as ESM modules. You can also go file by file, converting part of your app to ESM without needing to rewrite everything at once. For files still using CommonJS, they can continue using `require()` and `module.exports` while coexisting with the ESM-based parts.

{% hint style="warning" %}
Mixing ESM and CJS: Be cautious when mixing ESM and CommonJS. ESM supports asynchronous loading, while CJS is synchronous, so certain patterns may behave differently.

**Example of loading a CommonJS module in ESM:**

```javascript
// Load CommonJS module dynamically
const cjsModule = await import('cjs-module');
```

{% endhint %}

## Migrating from CommonJS to ESM

If you're updating an existing Homey app that uses CommonJS, here are some steps to migrate it to ESM:

1. Replace `require()` with `import`:

```javascript
const Homey = require('homey');  // Before
import Homey from 'homey';       // After
```

2. Replace `module.exports` with `export`:

```javascript
module.exports = MyApp;  // Before
export default MyApp;    // After
```

3. Check for compatibility: Make sure any third-party modules you are using also support ESM.
4. Upgrade your app's compatibility to **v12.0.1** in app.json.

## Common Gotchas and Compatibility

When switching to ESM, there are some important considerations:

1. **No `require()` in ESM**: If you need to load a CommonJS module from an ESM module, you'll have to use `import()` as `require()` is not available in ESM.
2. **No `__dirname` or `__filename` in ESM**: In ESM, `__dirname` and `__filename` are not available. You can use the following workaround:

```javascript
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
```

3. **Mixing ESM and CJS**: Be cautious when mixing ESM and CommonJS. ESM supports asynchronous loading, while CJS is synchronous, so certain patterns may behave differently.

## Benefits of Using ESM

Here are some reasons why using ESM in your Homey apps can be advantageous:

* **Asynchronous Module Loading**: ESM supports asynchronous loading, which can lead to performance improvements, especially in environments where modules are loaded on-demand.
* **Compatibility with Modern Syntax**: ESM is the standard for JavaScript moving forward, and its syntax is more aligned with other JavaScript features such as `import` and `export`.
* **Improved Developer Experience**: ESM simplifies code structure with a cleaner and more intuitive module syntax. Features like named exports make it easier to track what's being imported/exported.
* **Better Support for Static Analysis**: ESM allows tools and IDEs to analyze the structure of your code more effectively, leading to better autocomplete, refactoring, and error-checking features.
* **Native Support in Browsers**: ESM is natively supported in modern browsers without the need for bundlers like Webpack. This can simplify app development for projects that need to run in both Node.js and browser environments.
* **Strict Mode by Default**: All ESM modules run in strict mode by default, enforcing a stricter syntax and catching more common mistakes at runtime.
* **Top-Level `await`**: ESM allows the use of `await` at the top level, which simplifies asynchronous code in modules without needing to wrap everything in async functions.
* **Future-Proof**: ESM is the future of JavaScript module management, meaning you'll be aligned with future developments in the JavaScript ecosystem.


# Hardware Discount

To aid Homey Community Developers in creating & testing their apps on the right hardware, we offer a discount on purchasing hardware for eligible developers.

### Eligibility

* You must have developed at least one app, that has been approved & published in the Homey App Store.
* You may not have previously made use of this discount. It's limited to one purchase per developer.

### Pricing

| Product                | Discounted Price |
| ---------------------- | ---------------- |
| Homey Pro (Early 2023) | €225             |
| Homey Bridge (2022)    | €49              |

These costs are excluding shipping fee. The device might be refurbished with visible damage on the outside. The inner parts are verified to work.

### How to buy

To request a purchase order, please send an e-mail to <community-developer-discount@athom.com> from the same e-mail address as your developer account. In your e-mail, please let us know the product or products you'd like to purchase, and to which country they have to be shipped.

Our team will verify your request, and once approved, reply with a payment webpage to complete your order for the discounted price.


# Node.js 22 Upgrade Guide

Homey Apps now run in a Node.js v22 environment (Homey v12.9.0+), upgraded from Node.js v16 and v18 ([Learn more](https://apps.developer.homey.app/upgrade-guides/pages/-MWdHNw4mqB6t3ogwvDa#node.js)). While Node.js updates are generally backwards compatible, some issues may arise. Below are the currently known issues and solutions listed.

## Known issues

### HTTP(S) calls failing due to "socket hang up"/"ECONNRESET" when using node-fetch

Node.js 19 introduced changes to the management of keep-alive sockets. When using node-fetch, this might cause an `ECONNRESET` error under certain circumstances, particularly with services that aggressively close idle connections:

```javascript
FetchError: request to <> failed, reason: socket hang up
    at ClientRequest.<anonymous> (file:///app/node_modules/node-fetch/src/index.js:109:11)
    at ClientRequest.emit (node:events:519:28)
    at emitErrorEvent (node:_http_client:105:11)
    at Socket.socketOnEnd (node:_http_client:542:5)
    at Socket.emit (node:events:531:35)
    at endReadableNT (node:internal/streams/readable:1698:12)
    at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
  type: 'system',
  errno: 'ECONNRESET',
  code: 'ECONNRESET',
  erroredSysCall: undefined
}
```

For more information see this GitHub [issue](https://github.com/node-fetch/node-fetch/issues/1735).

#### Solution 1: Use a custom HTTP Agent (recommended for node-fetch)

Provide a custom `http.Agent` or `https.Agent` instance to your node-fetch requests. This ensures proper keep-alive socket management:

```javascript
const fetch = require("node-fetch");
const http = require("http");
const https = require("https");

// Create an agent with keep-alive enabled
const httpAgent = new http.Agent({ keepAlive: true });
const httpsAgent = new https.Agent({ keepAlive: true });

// Use the appropriate agent for your request
fetch("https://example.com/api", {
  agent: (_parsedURL) =>
    _parsedURL.protocol === "http:" ? httpAgent : httpsAgent,
});
```

#### Solution 2: Switch to built-in fetch (recommended for new code)

Node.js 18+ includes a native `fetch()` implementation that handles socket management automatically:

```javascript
// No imports needed, fetch is globally available
const response = await fetch("https://example.com/api");
const data = await response.json();
```

The built-in `fetch` is the preferred approach for new code as it's maintained as part of Node.js and doesn't require external dependencies.

### Missing Host header causing 400 Bad Request

As of Node.js 20, the default HTTP server started requiring a `Host` header to be present on incoming requests. If it is not present it will respond with a 400 Bad Request.

#### Solution

Either add the `Host` header to the requests, or disable the requirement on the server-side:

```javascript
http.createServer({ requireHostHeader: false });
```

For more information see this GitHub [pull request](https://github.com/athombv/com.athom.homeyduino/pull/60).

### Maximum call stack size exceeded when using node-homey-api

Due to socket.io using native Node.js sockets which as of Node.js 22 behave somewhat different, the following error can be observed when node-homey-api tries to close the socket connection:

```
Maximum call stack size exceeded {"stack":"RangeError: Maximum call stack size exceeded\n    at emitInitScript (node:internal/async_hooks:495:24)\n    at process.nextTick (node:internal/process/task_queues:143:5)\n    at emitUncaughtException (node:internal/event_target:1090:11)\n    at [nodejs.internal.kHybridDispatch] (node:internal/event_target:824:9)\n    at WebSocket.dispatchEvent (node:internal/event_target:751:26)\n    at fireEvent (node:internal/deps/undici/undici:11340:14)\n    at failWebsocketConnection (node:internal/deps/undici/undici:11421:9)\n    at closeWebSocketConnection (node:internal/deps/undici/undici:11692:9)\n    at WebSocket.close (node:internal/deps/undici/undici:12352:9)\n    at WS.doClose (file:///../node_modules/engine.io-client/build/esm-debug/transports/websocket.js:83:21)"}
```

**Solution:**

Update node-homey-api to version 3.14.17 or newer.


# Homey v6.0.0

How to upgrade to Homey v6.0.0

Homey v6.0.0 adds several updates to the Bluetooth Low Energy (BLE) SDK. Besides adding support for BLE notifications, also several bug fixes have been applied. With the changes in this update it will be much easier to create BLE apps for Homey.

{% hint style="info" %}
No breaking changes are present in this update - BLE apps that worked on Homey v5.0.0 will keep working on Homey v6.0.0
{% endhint %}

## BLE Notifications

The code snippet below shows an example of how to use Bluetooth Notifications in your app:

```javascript
// Subscribe to notifications
await characteristic.subscribeToNotifications((data) => {
  this.log("I received a notification: ", data);
});

// Wait for 5 seconds
await wait(5000);

// Unsubscribe from the notifications
await characteristic.unsubscribeFromNotifications();
```

## Disconnect event

Homey v6.0.0 reintroduces the 'disconnect' event. This method can be especially useful when used together with BLE notifications. The disconnect event is not always emitted when a device is disconnected, for example by turning it off, it is emitted whenever Homey *knows* that it is disconnected. This behaviour occurs from the fact that BLE devices may turn off their radio's while maintaining an active connection.

```javascript
// Register a callback for when the peripheral disconnects
peripheral.on("disconnect", () => {
  this.log("Disconnected from peripheral: ", peripheral.uuid);
});
```

> The disconnect event is *not guaranteed to trigger* on each disconnect. But if it *does* trigger then it is guaranteed that the peripheral is disconnected.

## Addressed BLE caching issues

The previous versions of the Bluetooth Low Energy SDK contained some caching issues which have been resolved. Discovery results will be kept in cache for **at least 30 seconds**.

A peripheral will not automatically disconnect anymore after 60 seconds. It will stay connected until the app it is used by closes, or when the corresponding device is removed by the user, or until `peripheral.disconnect()` is explicitly called.

The connection status of a peripheral is assumed to stay the same until the SDK receives an indication that suggests otherwise - therefore the `peripheral.state` may be incorrectly indicating a `connected` state when a device silently disconnects.

## (Deprecation) 128bit UUID convention

A common practice in BLE is to use shortened UUID's for common services and characteristics. Generally, a UUID used in BLE has 128 bits. However, if the UUID is formatted like the base UUID it can be shortened to a 16bit uuid by using the 4th to the 8th hexadecimal only. For example:

```
// 128bit UUID
'0000ABCD-0000-1000-8000-00805F9B34FB'

// 16bit UUID (Deprecated)
'ABCD'
```

Starting from Homey v6.0.0 it has been decided that Homey will use long UUID's for all BLE device by default. The reason for this is to prevent confusion - and also to ensure that the SDK can give consistent results when used for different devices on different Homey models.

> Short UUIDs are still supported and your current apps will keep working.


# Upgrading to SDK v3

How to upgrade to SDK v3 introduced in Homey v5.0.0

Homey 5.0.0 introduces a new SDK version. This SDK version removes the dual callback/promise support for API's and allows you to use `async/await` everywhere you want/need to. We hope supporting asynchronous calls everywhere will make it easier to develop and maintain Homey apps.

You can start using SDK v3 simply by updating the `sdk` property in your App Manifest from `2` to `3`. Since SDK v3 is introduced in Homey 5.0.0 the Homey `compatibility` field in your App Manifest should be changed to `>=5.0.0`.

{% code title="/.homeycompose/app.json" %}

```javascript
  "compatibility": ">=5.0.0"
```

{% endcode %}

{% hint style="warning" %}
With the introduction of a new SDK version we have discontinued the support for SDK version 1. Apps using SDK version 1 will be disabled starting with Homey 5.0.0.
{% endhint %}

Note that the SDK v2 versions of the Homey app libraries (`homey-oauth2app`, `homey-rfdriver`, `homey-meshdriver` and `homey-log`) are not compatible with SDK v3. Make sure to upgrade these libraries to versions that do support SDK v3. When using `homey-meshdriver` for Zigbee or Z-Wave, please look at the specific sections for [Z-Wave](#z-wave-and-meshdriver) and [Zigbee](#zigbee-and-meshdriver).

## Homey instance moved from `require('homey')` to `this.homey`

In previous versions of the SDK you could access the Homey API through `const Homey = require('homey')`. This is a very convenient way to give access to the managers anywhere it is needed however this prevents us from optimizing apps in the future as it pollutes the "global namespace". Therefore, the homey module now only exports the classes you might need such as `Homey.App`, `Homey.Driver` and `Homey.Device`. The module also contains your environment variables through `Homey.env`, and app manifest through `Homey.manifest`. All managers (the API you use to interact with Homey) have moved to `this.homey`, a property that is set on your App, Driver and Device instances. You can find the name of your manager in the `Homey` instance documentation.

Additionally, it is no longer needed to create and register resources. For example previously you would write the following code:

{% code title="/drivers/\<driver\_id>/driver.js" %}

```javascript
const Homey = require("homey");

class Driver extends Homey.Driver {
  onInit() {
    this.rainingCondition = new Homey.FlowCardCondition("is_raining");
    this.rainingCondition.register();

    this.myImage = new Homey.Image();
    this.myImage.setUrl("https://www.example.com/image.png");
    this.myImage.register().catch(this.error);
  }
}

module.exports = Driver;
```

{% endcode %}

In SDK v3 this turns into:

{% code title="/drivers/\<driver\_id>/driver.js" %}

```javascript
const Homey = require("homey");

class Driver extends Homey.Driver {
  async onInit() {
    this.rainingCondition = this.homey.flow.getConditionCard("is_raining");

    this.myImage = await this.homey.images.createImage();
    this.myImage.setUrl("https://www.example.com/image.png");
  }
}

module.exports = Driver;
```

{% endcode %}

{% hint style="warning" %}
For the same reason we have moved away from "polluting the global namespace", we urge you to define all variables as properties on your App, Driver or Device instances. Your global scope (anything that isn't inside of a class) should only contain constants (their values shouldn't be changed). Relying on global state may cause issues for your app in the future.
{% endhint %}

## Creating and triggering Flow cards

Since most apps supports custom Flow cards, below is an example of how to register and trigger Flow cards in SDK v3. See [Flow](/the-basics/flow) section for more details.

{% code title="/drivers/\<driver\_id>/driver.js" %}

```javascript
const Homey = require("homey");

class Driver extends Homey.Driver {
  async onInit() {
    // Register a Flow card to trigger a notification on a TV
    this.homey.flow
      .getActionCard("show_notification")
      .registerRunListener(async (args) => {
        return args.tv.createToast(args.message);
      });

    // Register a Device Flow card to launch an application on a TV
    this._flowTriggerAppLaunched = this.homey.flow
      .getDeviceTriggerCard("app_launched")
      .registerRunListener(async (args, state) => {
        return args.application.id === state.id;
      });

    // Register an autocomplete listener for the `application` argument of the `app_launched` flow card
    this._flowTriggerAppLaunched.registerArgumentAutocompleteListener(
      "application",
      async (query, args) => {
        return args.tv.autocompleteApplicationArgument(query);
      }
    );
  }

  // Trigger Flow's using the `app_lauched` card
  triggerAppLaunchedFlow(device, tokens, state) {
    this._flowTriggerAppLaunched
      .trigger(device, tokens, state)
      .catch(this.error);
  }
}

module.exports = Driver;
```

{% endcode %}

## Web API improvements

API routes now need to be defined in the App Manifest instead of the `api.js`. Additionally since you can no longer gain access to the API through require-ing homey it is now passed as an argument to your API handler method. Read more about the Homey App Web API in the [Web API guide](/advanced/web-api). Here is an example of the new structure for Homey App Web API's:

{% code title="/.homeycompose/app.json" %}

```javascript
  "api": {
    "getSomething": {
      "method": "get",
      "path": "/"
    },
  }
```

{% endcode %}

{% code title="/api.js" %}

```javascript
module.exports = {
  async getSomething({ homey, query }) {
    const result = await homey.app.getSomething();
    // ...
    return result;
  },
};
```

{% endcode %}

## Consistent APIs

We simplified some APIs with the goal of making them more consistent. This is to say we removed `Driver.getManifest()` and `Device.getDriver()` in favour of using properties: [`Driver#manifest`](https://apps-sdk-v3.developer.homey.app/Driver.html#manifest) and [`Device#driver`](https://apps-sdk-v3.developer.homey.app/Device.html#driver).

Additionally, the signature of [`Device#onSettings()`](https://apps-sdk-v3.developer.homey.app/Device.html#onSettings) has been changed to support destructuring: `onSettings({ oldSettings, newSettings, changedKeys })`.

## Promise-only APIs

In previous versions of the Apps SDK many methods supported both callbacks and Promises. In version 3, the support for callbacks has been removed from all places that previously supported both. You can reference the [SDK API documentation](https://apps-sdk-v3.developer.homey.app) to find the signature of all methods.

### Async pairing socket

The argument that is passed to [`Driver#onPair()`](https://apps-sdk-v3.developer.homey.app/Driver.html#onPair) has been changed. Previously this argument was an `EventEmitter` with an `.on` method that would receive a callback as its last argument. In order to offer better support for promises this was changed to a `PairSession` that has a `.setHandler` method where it is possible to return a Promise.

In SDK v2 you might have implemented an `onPair` method like this:

{% code title="/drivers/\<driver\_id>/driver.js" %}

```javascript
const Homey = require('homey');

class Driver extends Homey.Driver {
  onPair(session) {
    socket.on("my_event", (data, callback) => {
      this.log("data", data);

      callback(null, "reply");
    });
  }
}

module.exports = Driver;
```

{% endcode %}

In SDK v3 this turns into:

{% code title="/drivers/\<driver\_id>/driver.js" %}

```javascript
const Homey = require('homey');

class Driver extends Homey.Driver {
  onPair(session) {
    session.setHandler("my_event", async (data) => {
      this.log("data", data);

      return "reply";
    });
  }
}

module.exports = Driver;
```

{% endcode %}

This also affects [`Driver#onPairListDevices()`](https://apps-sdk-v3.developer.homey.app/Driver.html#onPairListDevices). If you're app is overriding this method you can upgrade by removing the callback, making it async and returning the device list.

In SDK v2 you might have implemented an `onPairListDevices` method like this:

{% code title="/drivers/\<driver\_id>/driver.js" %}

```javascript
const Homey = require('homey');

class Driver extends Homey.Driver {
  onPairListDevices(data, callback) {
    const discoveryStrategy = this.getDiscoveryStrategy();
    const discoveryResults = Object.values(
      discoveryStrategy.getDiscoveryResults()
    );

    const devices = discoveryResults.map((discoveryResult) => {
      return {
        name: discoveryResult.txt.name,
        data: {
          id: discoveryResult.id,
        },
      };
    });

    callback(null, devices);
  }
}

module.exports = Driver;
```

{% endcode %}

In SDK v3 this turns into:

{% code title="/drivers/\<driver\_id>/driver.js" %}

```javascript
const Homey = require('homey');

class Driver extends Homey.Driver {
  async onPairListDevices() {
    const discoveryStrategy = this.getDiscoveryStrategy();
    const discoveryResults = Object.values(
      discoveryStrategy.getDiscoveryResults()
    );

    const devices = discoveryResults.map((discoveryResult) => {
      return {
        name: discoveryResult.txt.name,
        data: {
          id: discoveryResult.id,
        },
      };
    });

    return devices;
  }
}

module.exports = Driver;
```

{% endcode %}

## App#onInit() called before Driver and Device onInit()

In SDK v2 your [`App#onInit()`](https://apps-sdk-v3.developer.homey.app/App.html#onInit) method would be executed after all managers where ready. Unfortunately this also meant that your Driver and Device `onInit` methods where executed before your [`App#onInit()`](https://apps-sdk-v3.developer.homey.app/App.html#onInit). This ordering was somewhat confusing so we changed the order in which we call the `onInit` methods in SDK v3.

In an app with an `app.js` and two drivers (`driver-one` and `driver-two` both have a single device) the order of `onInit` calls is now:

1. [`App#onInit()`](https://apps-sdk-v3.developer.homey.app/App.html#onInit)
2. [`Driver#onInit()`](https://apps-sdk-v3.developer.homey.app/Driver.html#onInit) (`driver-one`)
3. [`Device#onInit()`](https://apps-sdk-v3.developer.homey.app/Device.html#onInit)
4. [`Driver#onInit()`](https://apps-sdk-v3.developer.homey.app/Driver.html#onInit) (`driver-two`)
5. [`Device#onInit()`](https://apps-sdk-v3.developer.homey.app/Device.html#onInit)

The consequence of this change is that in your [`App#onInit()`](https://apps-sdk-v3.developer.homey.app/App.html#onInit) you cannot access Drivers (`this.homey.drivers.getDriver()` will throw an error). Instead of accessing Drivers from your App you can use [`App#onInit()`](https://apps-sdk-v3.developer.homey.app/App.html#onInit) to set up any data or classes that you might need in your application, then when your Driver or Device's `onInit` methods are called you are able to access this data directly through `this.homey.app`.

## Capabilities

The capabilities `alarm_contact` and `alarm_motion` activated a zone when an alarm is triggered. As of Homey v5.0.0 it is possible to disable this behaviour by using the capability option `zoneActivity`. This option accepts a boolean and defaults to `true`. This makes it possible to disable zone activity for these capabilities.

### Promises in App settings / Custom pair views

All methods in the Custom pair and App settings views now support callbacks and promises. The guides have been updated to show the usage with promises but callbacks remain supported for now.

{% hint style="info" %}
It is advised to update your code to use promises only for any API, because callbacks will be removed in a later SDK version.
{% endhint %}

## Z-Wave and MeshDriver

### Promises

Since all APIs are now promise-only, the way to interact with a Z-Wave node from within your app is promise-only as well. For example, previously you could execute a command like this:

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const { ZwaveDevice } = require('homey-meshdriver');

class Device extends ZwaveDevice {
  onMeshInit() {
    this.node.CommandClass.COMMAND_CLASS_BASIC.BASIC_SET(
      { Value: true },
      (err, result) => {
        // command has been executed
      }
    );
  }
}

module.exports = Device;
```

{% endcode %}

When using SDK version 3 this is no longer possible, and you should use promises only:

{% code title="/drivers/\<driver\_id>/device.js" %}

```javascript
const { ZwaveDevice } = require('homey-zwavedriver');

class Device extends ZwaveDevice {
  async onNodeInit() {
    await this.node.CommandClass.COMMAND_CLASS_BASIC.BASIC_SET({ Value: true });
    // command has been executed
  }
}

module.exports = Device;
```

{% endcode %}

### MeshDriver

Additionally, the [MeshDriver](https://github.com/athombv/node-homey-meshdriver) library is only compatible with SDK version 2. In order to create drivers for Z-Wave devices on Homey v5.0.0, a new (Z-Wave only) library has been made available for SDK version 3: [ZwaveDriver](https://github.com/athombv/node-homey-zwavedriver). Breaking changes are kept to a minimum to reduce the amount of effort to implement [ZwaveDriver](https://github.com/athombv/node-homey-zwavedriver) over MeshDriver. The most important changes can be found in the [documentation](https://athombv.github.io/node-homey-zwavedriver/).

### Change to associationGroups behaviour

The behaviour of the `zwave.associationGroups` driver property has changed in Homey v5.0.0 to be more predictable:

* `associationGroups: []` will now remove the default association group 1 (Z-Wave Plus lifeline)
* Not specifying `associationGroups` will now set the default association group 1 (Z-Wave Plus lifeline)

{% hint style="warning" %}
If you are updating your app you should make sure that drivers that either do not specify `associationGroups` or that set `associationGroups` to an empty array still behave correctly on Homey v5.0.0.
{% endhint %}

## Zigbee and MeshDriver

### Improved Zigbee stack

With Homey v5.0.0 comes a new and improved, built from scratch, Zigbee software stack. Zigbee apps developed for SDK version 2 will have to be updated to SDK version 3 in order to run on Homey v5.0.0 and higher.

### MeshDriver

Similar to Z-Wave, Zigbee will also come with a new (Zigbee-only) library to make developing Zigbee drivers a breeze: [ZigbeeDriver](https://github.com/athombv/node-homey-zigbeedriver). The breaking changes are kept to a minimum to reduce the amount of effort to implement [ZigbeeDriver](https://github.com/athombv/node-homey-zigbeedriver) over MeshDriver. Take a look at the [documentation](https://athombv.github.io/node-homey-zigbeedriver/) to find out about the most important changes.

Additionally, we created another library specifically for clusters: [ZigbeeClusters](https://github.com/athombv/node-zigbee-clusters). It is implemented by [ZigbeeDriver](https://github.com/athombv/node-homey-zigbeedriver) and is the place where all the Zigbee clusters are defined. In the case you need to add new Zigbee clusters, update existing clusters or are looking at more advanced features such as implementing custom clusters, take a look at the [documentation](https://athombv.github.io/node-zigbee-clusters/).

{% hint style="info" %}
We have updated the Zigbee guide and added a guide to upgrade from SDK version 2 to SDK version 3.
{% endhint %}

## App timezone now always UTC

In SDK v3 the default timezone (`process.env.TZ`) will always be set to `UTC`. In SDK v2, the timezone of an app would match the timezone the user set in their settings. This behaviour was confusing and could cause correct apps to have bugs when a user changes their timezone. To have more consistent and predictable behaviour all dates in SDK v3 apps will now default to `UTC`.

Some examples of the code that is affected by this change are:

* `new Date('3/14/20')`
* `Date.parse('3/14/20')`
* `myDate.getHours()`
* `myDate.setHours(12)`
* ...

Here's a full example how to retrieve the current Homey's timezone, and log a localized time string.

```javascript
const timezone = await this.homey.clock.getTimezone(); // e.g. Europe/Amsterdam
const formatter = new Intl.DateTimeFormat([], {
  timeZone: timezone,
  hour: '2-digit',
  minute: '2-digit',
  hour12: false, // Use 24-hour format
});

const timeParts = formatter.formatToParts(new Date());
const hour = timeParts.find(part => part.type === 'hour').value;
const minute = timeParts.find(part => part.type === 'minute').value;

this.log(`The time is ${hour}:${minute}`); // e.g. The time is 13:37
```

## ManagerCron removal

ManagerCron has been removed. We advice you to use `this.homey.setTimeout`, `this.homey.clearTimeout`, `this.homey.setInterval` and `this.homey.clearInterval` instead. These are the same as the native variants, but will take care of clearing the timeouts/intervals themselves when the app gets removed. Alternatively you could use `this.homey.on('unload', () => clearInterval(myInterval))`.

## Removed deprecated APIs

The previously deprecated `Image.format`, `Image.getFormat()`, `Image.getBuffer()` and `Image.setBuffer()` APIs have been removed. More information can be found in the [Image Api guide](/advanced/images).


# Zigbee Apps

How to upgrade your Zigbee App to the new Zigbee API introduced in Homey 5.0.0

## Updating dependencies

Install [homey-zigbeedriver](https://athombv.github.io/node-homey-zigbeedriver/) and [zigbee-clusters](https://github.com/athombv/node-zigbee-clusters):

```bash
npm install --save homey-zigbeedriver zigbee-clusters
```

And uninstall [homey-meshdriver](https://github.com/athombv/node-homey-meshdriver):

```bash
npm uninstall homey-meshdriver
```

## Updating the driver manifest

The most notable change here is the addition of the `endpoints` property which needs to contain the endpoint definition. Additionally, the number of properties which are used to identify a Zigbee device have been reduced, and only the following two are required:

* `productId`
* `manufacturerId`

Therefore, the following can be removed:

* `deviceId`
* `profileId`

To discover the endpoint definition of your Zigbee device, use the "interview" button in the [Zigbee developer tools](https://tools.developer.homey.app/tools/zigbee). It is not safe to assume the endpoint ids you might have used before are equal. It is advised to test this with your device, since an incorrect endpoint definition will result in a non-functioning device.

{% hint style="warning" %}
The endpoint definition is currently dynamic such that it does not require a repair for it to be updated on the device. In the future this might change.
{% endhint %}

For more information on how to add the `endpoints` definition see the [Zigbee guide](/wireless/zigbee).

## Updating drivers

For many drivers it will be very easy to update SDK v3 and `homey-zigbeedriver`.

If your driver extends `ZigBeeLightDevice` it is as easy as replacing the imports from:

```javascript
const { ZigBeeLightDevice } = require('homey-meshdriver');

class DimmableBulb extends ZigBeeLightDevice {}

module.exports = DimmableBulb;
```

To:

```javascript
const { ZigBeeLightDevice } = require('homey-zigbeedriver');

class DimmableBulb extends ZigBeeLightDevice {}

module.exports = DimmableBulb;
```

If your driver extends `ZigBeeDevice` the first step is to replace the `homey-meshdriver` imports with `homey-zigbeedriver`, for example:

```javascript
const { ZigBeeDevice } = require('homey-zigbeedriver');

class MyZigBeeDevice extends ZigBeeDevice {}

module.exports = MyZigBeeDevice;
```

The next steps depends on the implementation of the driver:

* For directly sending commands to the node check the Zigbee guide > Driver and Device > Commands.
* For registering capabilities check the Zigbee guide > Driver and Device > Capabilities.
* For configuration attribute reporting check the Zigbee guide > Driver and Device > Attribute Reporting.
* For bindings and groups (new!) check the Zigbee guide > Driver and Device > Bindings and Groups.
* For implementing custom clusters (new!) check the Zigbee guide > Driver and Device > Custom Cluster.
* If your app implements custom Flows or accesses `Managers` for other reasons, check the updated SDK v3 documentation.

## Examples

In case you need some inspiration, or need an example, take a look at the following example app which has been migrated to SDK v3 and `homey-zigbeedriver` [com.ikea.tradfri](https://github.com/athombv/com.ikea.tradfri-example).

## Breaking changes for homey-zigbeedriver

This is a non exhaustive list of breaking changes in `homey-zigbeedriver` with respect to `homey-meshdriver` which might be good to be aware of:

* MeshDevice is removed in favour of ZigBeeDevice.
* `onMeshInit()` is deprecated in favour of `onNodeInit()`.
* `this.node.on(‘online’)` is removed in favour of `this.onEndDeviceAnnounce()`.
* `getClusterEndpoint` returns `null` if not found.
* `cluster` property is changed from string value (e.g. `genOnOff`) to an object which is exported by `const { CLUSTER } = require(‘zigbee-clusters’);`
* `registerReportListener` is deprecated in favour of `BoundCluster` implementation.
* `registerAttrReportListener` is deprecated in favour of `configureAttributeReporting`.
* `calculateZigbeeDimDuration` renamed to `calculateLevelControlTransitionTime`.
  * `calculateColorControlTransitionTime` is added for the `colorControl` cluster.
* `ZigBeeXYLightDevice` is removed in favour of `ZigBeeLightDevice`, it detects if the light supports hue and saturation or XY only.


# Device Capabilities

Starting from version 12.2.0, Homey Pro (Early) 2023 will shift its approach to favour custom capabilities over system capabilities, aligning with the behaviour already seen in Homey Pro 2016-2019.

Currently, when a custom capability on Homey Pro (Early) 2023 shares an ID with a system capability, system Flow cards are generated for that device. However, with the upcoming change, these Flow cards will no longer be available.

To ensure that existing Flows continue to work seamlessly, apps that use system IDs for custom capabilities will need to update their drivers. There are two ways to make sure nothing in your app breaks. The easiest way is to remove your custom capability from your .homeycompose. This will instead make Homey use the system capability and generate the system Flow cards.

### Keeping your custom capability

You can also choose to keep your custom capability. This can be useful if it differs from the system capability. To avoid breaking Flow cards, make sure to add a flow\.compose.json to the effected Drivers containing the Flow cards with the same Flow IDs.

The capability IDs listed below are currently used in various apps as custom capabilities. Each file contains the Flow card ID's for that capability. By copying these Flow card ID's and adding them to your drivers flow\.compose.json all Flows should keep running.

### Device Flow Cards

When your app has multiple drivers that require the same device Flow card, you can put the Flow card definition in your .homeycompose. You can then add a device argument and a filter to make the Flow card appear for the right drivers. See [device Flow arguments](/the-basics/flow/arguments#device) for more information.

{% code title="/.homeycompose/flow/actions/disco\_mode.json" %}

```json
{
  "title": { "en": "Disco mode" },
  "args": [
    {
      "type": "device",
      "name": "device",
      "filter": "driver_id=my_driver"
    }
  ]
}
```

{% endcode %}

### Registering Run Listeners

For some Flow cards you will also need to register the correct listeners. The listeners for each capability can be found below the corresponding files. These listeners can be added to your app in the app.js `onInit()` function.

### Duration

If you have enabled duration for any capabilities through the capability options, you will also have to add `duration: true` to each action Flow card that should support it in your `driver.flow.compose.json`.

{% code title="/drivers/\<driver\_id>/driver.flow\.compose.json" %}

```json
{
  "actions": [
    {
      "id": "on",
      "highlight": true,
      "duration": true,
      "title": { "en": "Turn on" }
    }
  ]
}
```

{% endcode %}

### Capabilities

{% file src="/files/isWe60mVHsaKmZGNNYlw" %}

{% code title="/app.js" %}

```javascript
this.homey.flow.getConditionCard('alarm_contact').registerRunListener((args, state) => {
  return args.device.getCapabilityValue('alarm_contact');
});
```

{% endcode %}

{% file src="/files/m4sVGpY0gNOxgrxaqXdv" %}

{% code title="/app.js" %}

```javascript
this.homey.flow.getConditionCard('alarm_generic').registerRunListener((args, state) => {
  return args.device.getCapabilityValue('alarm_generic');
});
```

{% endcode %}

{% file src="/files/E2BcuC3xlcgctx9Ov2Nt" %}

{% code title="/app.js" %}

```javascript
this.homey.flow.getConditionCard('alarm_motion').registerRunListener((args, state) => {
  return args.device.getCapabilityValue('alarm_motion');
});
```

{% endcode %}

{% file src="/files/QBfxmkPwHMYweGv6jxSa" %}

{% code title="/app.js" %}

```javascript
this.homey.flow.getConditionCard('alarm_smoke').registerRunListener((args, state) => {
  return args.device.getCapabilityValue('alarm_smoke');
});
```

{% endcode %}

{% file src="/files/I8db7HxErSWiQysoaaQ4" %}

{% code title="/app.js" %}

```javascript
this.homey.flow.getConditionCard('alarm_tamper').registerRunListener((args, state) => {
  return args.device.getCapabilityValue('alarm_tamper');
});
```

{% endcode %}

{% file src="/files/GpRP2hloLftf0cdO0JDQ" %}

{% code title="/app.js" %}

```javascript
this.homey.flow.getDeviceTriggerCard('homealarm_state_changed').registerRunListener((args, state) => {
  return args.device.getCapabilityValue('homealarm_state') === args.state;
});
this.homey.flow.getConditionCard('homealarm_state_is').registerRunListener((args, state) => {
  return args.device.getCapabilityValue('homealarm_state') === args.state;
});
this.homey.flow.getActionCard('set_homealarm_state').registerRunListener((args, state) => {
  return args.device.setCapabilityValue('homealarm_state', args.state);
});
```

{% endcode %}

{% file src="/files/3Vyor7Cb4YWjqtjqo0Cz" %}

{% file src="/files/FHBcqAO17xJoimygzWwC" %}

{% file src="/files/cWDdOyTPNYSeHThlmY7A" %}

{% file src="/files/IStzNPmUQvRetZWzKO2O" %}

{% file src="/files/Drn42zO3VL0duXR0d1AW" %}

{% file src="/files/uDBDGTycrhg1nIlMsDwe" %}

{% file src="/files/Lh7f3jeBXOGxqZM9SwE9" %}

{% file src="/files/LM7RcOi9SN6W6k4CqnUr" %}

{% file src="/files/vAz1dEyNRo8foRDrVDdg" %}

{% file src="/files/jt1TbWJ9vTWUsqavtL7I" %}

{% file src="/files/hS2mi7I9VW1LRHeVNOqF" %}

{% file src="/files/fsQzAhvFFNNkTjct5KgV" %}

{% file src="/files/WoigoJfqpiG7IO4Wp7DC" %}

{% file src="/files/36MzzSzojDAyLn2IMTqd" %}

{% file src="/files/ReQee6KbmIRx69qp01T8" %}

{% file src="/files/0YNlrexzbUddlLWv3QLC" %}

{% file src="/files/pnrjmQUiFMIv4PupgvoH" %}

{% file src="/files/TbVqChRuA9kJg2fIyuwA" %}

{% code title="/app.js" %}

```javascript
this.homey.flow.getConditionCard('on').registerRunListener((args, state) => {
  return args.device.getCapabilityValue('onoff');
});
this.homey.flow.getConditionCard('open').registerRunListener((args, state) => {
  return args.device.getCapabilityValue('onoff');
});
this.homey.flow.getActionCard('on').registerRunListener((args, state) => {
  return args.device.setCapabilityValue('onoff', true);
});
this.homey.flow.getActionCard('off').registerRunListener((args, state) => {
  return args.device.setCapabilityValue('onoff', false);
});
this.homey.flow.getActionCard('toggle').registerRunListener((args, state) => {
  const value = args.device.getCapabilityValue('onoff');
  return args.device.setCapabilityValue('onoff', !value);
});
this.homey.flow.getActionCard('open').registerRunListener((args, state) => {
  return args.device.setCapabilityValue('onoff', true);
});
this.homey.flow.getActionCard('close').registerRunListener((args, state) => {
  return args.device.setCapabilityValue('onoff', false);
});
```

{% endcode %}

{% file src="/files/aVcXgqCv3Gc5kKS9DA9Q" %}

{% code title="/app.js" %}

```javascript
this.homey.flow.getActionCard('target_temperature_set').registerRunListener((args, state) => {
  return args.device.setCapabilityValue('target_temperature', args.target_temperature);
});
```

{% endcode %}

{% file src="/files/jH5xP6ShxUfuTryiqmu0" %}

{% code title="/app.js" %}

```javascript
this.homey.flow.getDeviceTriggerCard('thermostat_mode_changed').registerRunListener((args, state) => {
  return args.device.getCapabilityValue('thermostat_mode') === args.thermostat_mode;
});
this.homey.flow.getConditionCard('thermostat_mode_is').registerRunListener((args, state) => {
  return args.device.getCapabilityValue('thermostat_mode') === args.thermostat_mode;
});
this.homey.flow.getActionCard('thermostat_mode_set').registerRunListener((args, state) => {
  return args.device.setCapabilityValue('thermostat_mode', args.thermostat_mode);
});
```

{% endcode %}


