mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 15:18:41 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8066e8da8 | ||
|
|
ec28738784 | ||
|
|
8398bcb51d | ||
|
|
c0143cb40d | ||
|
|
e4c939a643 | ||
|
|
4f1a0ae9cd | ||
|
|
fb2e149a41 | ||
|
|
29f90b77b4 | ||
|
|
954b891698 | ||
|
|
7ea27b51e2 | ||
|
|
02e198eca2 | ||
|
|
e5e8c58903 | ||
|
|
aebef20536 | ||
|
|
a519577cf7 | ||
|
|
f9076b362d | ||
|
|
c2e28eaee1 | ||
|
|
14712d6fe1 | ||
|
|
ff0b812029 | ||
|
|
afbbf70caa | ||
|
|
f85722da38 | ||
|
|
e97fc903a5 | ||
|
|
81db31b784 | ||
|
|
ff2fe2b358 | ||
|
|
fbecb7e0d2 | ||
|
|
1198a2d9ac | ||
|
|
7b74971006 | ||
|
|
3cdf7604a0 | ||
|
|
516d53e9df | ||
|
|
6c6dfc85cd | ||
|
|
8a6a7409be | ||
|
|
6345cb07bf | ||
|
|
35be95115d | ||
|
|
852bca5a08 | ||
|
|
8aa3cf0c29 | ||
|
|
3e071a73df | ||
|
|
b4cfa7b0b7 | ||
|
|
9a6397fe75 | ||
|
|
22c24a16b3 | ||
|
|
3b29266be3 | ||
|
|
0fddfc9ffa | ||
|
|
b2e588c1cf | ||
|
|
ded3a58883 | ||
|
|
a5b03b5dad | ||
|
|
469ddb7e76 | ||
|
|
64022897f8 | ||
|
|
274109bc85 | ||
|
|
b9c52ef96e | ||
|
|
9474dfe249 | ||
|
|
a7f990ef95 | ||
|
|
ae93e6fd0f | ||
|
|
946ebe16b6 | ||
|
|
a0c895a88d | ||
|
|
44b26f9b22 | ||
|
|
ef6572dfd0 | ||
|
|
7c6fe4d95e | ||
|
|
f5e5c76ca9 | ||
|
|
0d9ed11ceb | ||
|
|
b9ba635bea | ||
|
|
88521b25d3 | ||
|
|
efc971d2ca | ||
|
|
7d27e6db69 | ||
|
|
812c4927e7 | ||
|
|
d7b678e636 | ||
|
|
00f836fce6 | ||
|
|
992ebafc63 | ||
|
|
f0068d73cb | ||
|
|
867918bd63 | ||
|
|
1fa547437f | ||
|
|
ad13564687 | ||
|
|
b91b456f98 | ||
|
|
a01048487d | ||
|
|
2d3ab1cf8a |
@@ -3,7 +3,7 @@ Before submitting an issue make sure you've searched for a similar issue and rea
|
||||
|
||||
Rules (don't ignore these, your issue will be closed without further notice):
|
||||
- English only
|
||||
- Make sure you are on the latest Zigbee2mqtt version
|
||||
- Make sure you are on the latest Zigbee2MQTT version
|
||||
- Provide a clear description of the problem
|
||||
- DON'T copy logs directly here, post a link to https://hastebin.com/ or https://pastebin.com/.
|
||||
- Make sure you are running the latest firmware https://github.com/koenkk/z-stack-firmware.
|
||||
@@ -11,7 +11,7 @@ Rules (don't ignore these, your issue will be closed without further notice):
|
||||
Did you read the FAQ?
|
||||
- https://www.zigbee2mqtt.io/information/FAQ.html
|
||||
|
||||
Zigbee2mqtt fails to start?
|
||||
Zigbee2MQTT fails to start?
|
||||
- https://www.zigbee2mqtt.io/information/FAQ.html#help-zigbee2mqtt-fails-to-start
|
||||
|
||||
Having issues when using a CC2531?
|
||||
@@ -41,6 +41,6 @@ Bug report?
|
||||
## How to reproduce it (minimal and precise)
|
||||
|
||||
## Debug Info
|
||||
Zigbee2mqtt version:
|
||||
Zigbee2MQTT version:
|
||||
Adapter hardware: CC2531, CC2530, CC26X2R1 or CC1352P-2
|
||||
Adapter firmware version:
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 30
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
@@ -10,6 +10,7 @@ jobs:
|
||||
- uses: actions/setup-node@v2-beta
|
||||
with:
|
||||
node-version: 12
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- name: Get NodeJS Version
|
||||
id: nodeversion
|
||||
run: |
|
||||
@@ -26,4 +27,50 @@ jobs:
|
||||
- name: Test
|
||||
run: npm run test-with-coverage
|
||||
- name: Lint
|
||||
run: npm run eslint
|
||||
run: npm run eslint
|
||||
- name: Docker login
|
||||
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
|
||||
run: echo ${{ secrets.DOCKER_KEY }} | docker login -u koenkk --password-stdin
|
||||
- name: Docker setup
|
||||
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
|
||||
run: |
|
||||
sudo wget -O /usr/local/bin/buildx https://github.com/docker/buildx/releases/download/v0.3.1/buildx-v0.3.1.linux-amd64
|
||||
sudo chmod a+x /usr/local/bin/buildx
|
||||
docker run --rm --privileged hypriot/qemu-register:v2.7.0
|
||||
buildx create --use
|
||||
buildx ls
|
||||
- name: Docker build dev
|
||||
if: github.ref == 'refs/heads/dev' && github.event_name == 'push'
|
||||
run: |
|
||||
buildx build \
|
||||
--build-arg COMMIT=$(git rev-parse --short HEAD) \
|
||||
--platform linux/arm64/v8,linux/amd64,linux/arm/v6,linux/arm/v7,linux/386 \
|
||||
-f docker/Dockerfile \
|
||||
--push \
|
||||
-t koenkk/zigbee2mqtt:latest-dev \
|
||||
.
|
||||
- name: Docker build release
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push'
|
||||
run: |
|
||||
TAG="$(git describe --tags)"
|
||||
buildx build \
|
||||
--build-arg COMMIT=$(git rev-parse --short HEAD) \
|
||||
--platform linux/arm64/v8,linux/amd64,linux/arm/v6,linux/arm/v7,linux/386 \
|
||||
-f docker/Dockerfile \
|
||||
--push \
|
||||
-t koenkk/zigbee2mqtt:latest -t "koenkk/zigbee2mqtt:$TAG" \
|
||||
.
|
||||
- name: Publish to npm
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push'
|
||||
run: npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN }}
|
||||
- name: Trigger danielwelch/hassio-zigbee2mqtt build
|
||||
if: github.ref == 'refs/heads/dev' && github.event_name == 'push'
|
||||
run: |
|
||||
curl -s \
|
||||
--request POST \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{"definition": {"id": 1}}' \
|
||||
-u ${{ secrets.HASSIO_TRIGGER_SECRET }} \
|
||||
"https://dev.azure.com/danielwelch2101/hassio-zigbee2mqtt/_apis/build/builds?api-version=5.1"
|
||||
@@ -0,0 +1,16 @@
|
||||
name: "Close stale issues/pull requests"
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days'
|
||||
stale-pr-message: 'This pull request is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days'
|
||||
days-before-stale: 30
|
||||
days-before-close: 7
|
||||
@@ -19,4 +19,4 @@ jobs:
|
||||
with:
|
||||
commit-message: Update zigbee-herdsman to ${{ github.event.client_payload.version }}
|
||||
branch: "update_zh"
|
||||
title: Update zigbee-herdsman
|
||||
title: Update zigbee-herdsman to ${{ github.event.client_payload.version }}
|
||||
|
||||
@@ -19,4 +19,4 @@ jobs:
|
||||
with:
|
||||
commit-message: Update zigbee-herdsman-converters to ${{ github.event.client_payload.version }}
|
||||
branch: "update_zhc"
|
||||
title: Update zigbee-herdsman-converters
|
||||
title: Update zigbee-herdsman-converters to ${{ github.event.client_payload.version }}
|
||||
|
||||
@@ -7,7 +7,6 @@ coverage
|
||||
.jenkins.yml
|
||||
.codeclimate.yml
|
||||
.github
|
||||
azure-pipelines.yaml
|
||||
|
||||
#linters
|
||||
.jscsrc
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<br>
|
||||
<br>
|
||||
<div style="display: flex;">
|
||||
<a href="https://dev.azure.com/koenkk/zigbee/_build?definitionId=5">
|
||||
<img src="https://dev.azure.com/koenkk/zigbee/_apis/build/status/Koenkk.zigbee2mqtt?branchName=dev">
|
||||
<a href="https://github.com/Koenkk/zigbee2mqtt/actions?query=workflow%3ACI">
|
||||
<img src="https://github.com/koenkk/zigbee2mqtt/workflows/CI/badge.svg">
|
||||
</a>
|
||||
<a href="https://github.com/Koenkk/zigbee2mqtt/releases">
|
||||
<img src="https://img.shields.io/github/release/koenkk/zigbee2mqtt.svg">
|
||||
@@ -33,7 +33,7 @@
|
||||
<img src="https://img.shields.io/npm/v/zigbee2mqtt">
|
||||
</a>
|
||||
</div>
|
||||
<h1>Zigbee2mqtt 🌉 🐝</h1>
|
||||
<h1>Zigbee2MQTT 🌉 🐝</h1>
|
||||
<p>
|
||||
Allows you to use your Zigbee devices <b>without</b> the vendors bridge or gateway.
|
||||
</p>
|
||||
@@ -48,7 +48,7 @@ The [documentation](https://www.zigbee2mqtt.io/) provides you all the informatio
|
||||
If you aren't familiar with **Zigbee** terminology make sure you [read this](https://www.zigbee2mqtt.io/information/zigbee_network.html) to help you out.
|
||||
|
||||
## [Integrations](https://www.zigbee2mqtt.io/#integration)
|
||||
Zigbee2mqtt integrates well with (almost) every home automation solution because it uses MQTT. However the following integrations are worth mentioning:
|
||||
Zigbee2MQTT integrates well with (almost) every home automation solution because it uses MQTT. However the following integrations are worth mentioning:
|
||||
|
||||
<img align="left" height="100px" width="100px" src="https://user-images.githubusercontent.com/7738048/40914297-49e6e560-6800-11e8-8904-36cce896e5a8.png">
|
||||
|
||||
@@ -67,7 +67,7 @@ Zigbee2mqtt integrates well with (almost) every home automation solution because
|
||||

|
||||
|
||||
### Internal Architecture
|
||||
Zigbee2mqtt is made up of three modules, each developed in its own Github project. Starting from the hardware (adapter) and moving up; [zigbee-herdsman](https://github.com/koenkk/zigbee-herdsman) connects to your Zigbee adapter an makes an API available to the higher levels of the stack. For e.g. Texas Instruments hardware, zigbee-herdsman uses the [TI zStack monitoring and test API](https://github.com/koenkk/zigbee-herdsman/raw/master/docs/Z-Stack%20Monitor%20and%20Test%20API.pdf) to communicate with the adapter. Zigbee-herdsman handles the core Zigbee communication. The module [zigbee-herdsman-converters](https://github.com/koenkk/zigbee-herdsman-converters) handles the mapping from individual device models to the Zigbee clusters they support. [Zigbee clusters](https://github.com/Koenkk/zigbee-herdsman/raw/master/docs/07-5123-06-zigbee-cluster-library-specification.pdf) are the layers of the Zigbee protocol on top of the base protocol that define things like how lights, sensors and switches talk to each other over the Zigbee network. Finally, the zigbee2mqtt module drives zigbee-herdsman and maps the zigbee messages to MQTT messages. Zigbee2mqtt also keeps track of the state of the system. It uses a `database.db` file to store this state; a text file with a JSON database of connected devices and their capabilities.
|
||||
Zigbee2MQTT is made up of three modules, each developed in its own Github project. Starting from the hardware (adapter) and moving up; [zigbee-herdsman](https://github.com/koenkk/zigbee-herdsman) connects to your Zigbee adapter an makes an API available to the higher levels of the stack. For e.g. Texas Instruments hardware, zigbee-herdsman uses the [TI zStack monitoring and test API](https://github.com/koenkk/zigbee-herdsman/raw/master/docs/Z-Stack%20Monitor%20and%20Test%20API.pdf) to communicate with the adapter. Zigbee-herdsman handles the core Zigbee communication. The module [zigbee-herdsman-converters](https://github.com/koenkk/zigbee-herdsman-converters) handles the mapping from individual device models to the Zigbee clusters they support. [Zigbee clusters](https://github.com/Koenkk/zigbee-herdsman/raw/master/docs/07-5123-06-zigbee-cluster-library-specification.pdf) are the layers of the Zigbee protocol on top of the base protocol that define things like how lights, sensors and switches talk to each other over the Zigbee network. Finally, the Zigbee2MQTT module drives zigbee-herdsman and maps the zigbee messages to MQTT messages. Zigbee2MQTT also keeps track of the state of the system. It uses a `database.db` file to store this state; a text file with a JSON database of connected devices and their capabilities.
|
||||
|
||||
## Supported devices
|
||||
See [Supported devices](https://www.zigbee2mqtt.io/information/supported_devices.html) to check whether your device is supported. There is quite an extensive list, including devices from vendors like Xiaomi, Ikea, Philips, OSRAM and more.
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
trigger:
|
||||
tags:
|
||||
include:
|
||||
- '*'
|
||||
branches:
|
||||
include:
|
||||
- '*'
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
|
||||
# Prepare
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '12.x'
|
||||
displayName: 'Install Node.js'
|
||||
- bash: npm ci
|
||||
displayName: 'Install dependencies'
|
||||
|
||||
# Verify
|
||||
- bash: npm run test-with-coverage
|
||||
displayName: 'Test'
|
||||
- bash: npm run eslint
|
||||
displayName: 'Lint'
|
||||
|
||||
# NPM
|
||||
- task: Npm@1
|
||||
inputs:
|
||||
command: publish
|
||||
publishEndpoint: npm
|
||||
displayName: 'Publish to npm'
|
||||
condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'), ne(variables['Build.Reason'], 'PullRequest'))
|
||||
|
||||
# Docker
|
||||
- task: DockerInstaller@0
|
||||
displayName: Docker install
|
||||
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
|
||||
inputs:
|
||||
dockerVersion: 19.03.5
|
||||
releaseType: stable
|
||||
- task: Docker@2
|
||||
displayName: Docker registry login
|
||||
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
|
||||
inputs:
|
||||
command: login
|
||||
containerRegistry: docker_registry
|
||||
- bash: |
|
||||
sudo wget -O /usr/local/bin/buildx https://github.com/docker/buildx/releases/download/v0.3.1/buildx-v0.3.1.linux-amd64
|
||||
sudo chmod a+x /usr/local/bin/buildx
|
||||
docker run --rm --privileged hypriot/qemu-register:v2.7.0
|
||||
buildx create --use
|
||||
buildx ls
|
||||
displayName: 'Docker setup'
|
||||
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
|
||||
- bash: |
|
||||
buildx build \
|
||||
--build-arg COMMIT=$(git rev-parse --short HEAD) \
|
||||
--platform linux/arm64/v8,linux/amd64,linux/arm/v6,linux/arm/v7,linux/386 \
|
||||
-f docker/Dockerfile \
|
||||
--push \
|
||||
-t koenkk/zigbee2mqtt:latest-dev \
|
||||
.
|
||||
displayName: 'Docker build dev'
|
||||
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/dev'), ne(variables['Build.Reason'], 'PullRequest'))
|
||||
- bash: |
|
||||
TAG="$(git describe --tags)"
|
||||
buildx build \
|
||||
--build-arg COMMIT=$(git rev-parse --short HEAD) \
|
||||
--platform linux/arm64/v8,linux/amd64,linux/arm/v6,linux/arm/v7,linux/386 \
|
||||
-f docker/Dockerfile \
|
||||
--push \
|
||||
-t koenkk/zigbee2mqtt:latest -t "koenkk/zigbee2mqtt:$TAG" \
|
||||
.
|
||||
displayName: 'Docker build release'
|
||||
condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'), ne(variables['Build.Reason'], 'PullRequest'))
|
||||
|
||||
# Trigger Home Assistant Zigbee2mqtt addon build
|
||||
- bash: |
|
||||
curl -s \
|
||||
--request POST \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{"definition": {"id": 1}}' \
|
||||
-u "$(HASSIO_TRIGGER_SECRET)" \
|
||||
"https://dev.azure.com/danielwelch2101/hassio-zigbee2mqtt/_apis/build/builds?api-version=5.1"
|
||||
displayName: 'Trigger Hass.io build'
|
||||
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/dev'), ne(variables['Build.Reason'], 'PullRequest'))
|
||||
@@ -3,7 +3,22 @@ const engines = require('./package.json').engines;
|
||||
|
||||
const version = engines.node;
|
||||
if (!semver.satisfies(process.version, version)) {
|
||||
console.log(`\t\tZigbee2mqtt requires node version ${version}, you are running ${process.version}!\n`); // eslint-disable-line
|
||||
console.log(`\t\tZigbee2MQTT requires node version ${version}, you are running ${process.version}!\n`); // eslint-disable-line
|
||||
}
|
||||
|
||||
// Validate settings
|
||||
const settings = require('./lib/util/settings');
|
||||
const errors = settings.validate();
|
||||
if (errors.length > 0) {
|
||||
console.log(`\n\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`);
|
||||
console.log(' READ THIS CAREFULLY\n');
|
||||
console.log(`Refusing to start because configuration is not valid, found the following errors:`);
|
||||
for (const error of errors) {
|
||||
console.log(`- ${error}`);
|
||||
}
|
||||
console.log(`\nIf you don't know how to solve this, read https://www.zigbee2mqtt.io/information/configuration.html`);
|
||||
console.log(`\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const Controller = require('./lib/controller');
|
||||
|
||||
+39
-24
@@ -9,6 +9,7 @@ const utils = require('./util/utils');
|
||||
const fs = require('fs');
|
||||
const data = require('./util/data');
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
|
||||
// Extensions
|
||||
const ExtensionPublish = require('./extension/publish');
|
||||
@@ -28,6 +29,13 @@ const ExtensionOnEvent = require('./extension/onEvent');
|
||||
const ExtensionOTAUpdate = require('./extension/otaUpdate');
|
||||
const ExtensionExternalConverters = require('./extension/externalConverters');
|
||||
|
||||
const AllExtensions = [
|
||||
ExtensionPublish, ExtensionReceive, ExtensionNetworkMap, ExtensionSoftReset, ExtensionHomeAssistant,
|
||||
ExtensionConfigure, ExtensionDeviceGroupMembership, ExtensionBridgeLegacy, ExtensionBridge, ExtensionGroups,
|
||||
ExtensionAvailability, ExtensionBind, ExtensionReport, ExtensionOnEvent, ExtensionOTAUpdate,
|
||||
ExtensionExternalConverters,
|
||||
];
|
||||
|
||||
class Controller {
|
||||
constructor() {
|
||||
this.zigbee = new Zigbee();
|
||||
@@ -36,6 +44,7 @@ class Controller {
|
||||
this.state = new State(this.eventBus);
|
||||
|
||||
this.publishEntityState = this.publishEntityState.bind(this);
|
||||
this.enableDisableExtension = this.enableDisableExtension.bind(this);
|
||||
this.onZigbeeAdapterDisconnected = this.onZigbeeAdapterDisconnected.bind(this);
|
||||
|
||||
// Initialize extensions.
|
||||
@@ -50,20 +59,17 @@ class Controller {
|
||||
new ExtensionBind(...args),
|
||||
new ExtensionOnEvent(...args),
|
||||
new ExtensionOTAUpdate(...args),
|
||||
new ExtensionReport(...args),
|
||||
];
|
||||
|
||||
if (settings.get().experimental.new_api) {
|
||||
this.extensions.push(new ExtensionBridge(...args));
|
||||
this.extensions.push(new ExtensionBridge(...args, this.enableDisableExtension));
|
||||
}
|
||||
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
this.extensions.push(new ExtensionBridgeLegacy(...args));
|
||||
}
|
||||
|
||||
if (settings.get().advanced.report) {
|
||||
this.extensions.push(new ExtensionReport(...args));
|
||||
}
|
||||
|
||||
if (settings.get().homeassistant) {
|
||||
this.extensions.push(new ExtensionHomeAssistant(...args));
|
||||
}
|
||||
@@ -91,22 +97,10 @@ class Controller {
|
||||
}
|
||||
|
||||
async start() {
|
||||
const settingsErrors = settings.validate();
|
||||
if (settingsErrors) {
|
||||
logger.error(`Refusing to start, configuration.yaml is not valid, found the following errors:`);
|
||||
for (const error of settingsErrors) {
|
||||
logger.error(`\t - ${error}`);
|
||||
}
|
||||
logger.error(
|
||||
`If you don't know how to solve this, read https://www.zigbee2mqtt.io/configuration/configuration.html`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
this.state.start();
|
||||
|
||||
const info = await utils.getZigbee2mqttVersion();
|
||||
logger.info(`Starting zigbee2mqtt version ${info.version} (commit #${info.commitHash})`);
|
||||
logger.info(`Starting Zigbee2MQTT version ${info.version} (commit #${info.commitHash})`);
|
||||
|
||||
// Start zigbee
|
||||
try {
|
||||
@@ -173,6 +167,23 @@ class Controller {
|
||||
await this.callExtensionMethod('onMQTTConnected', []);
|
||||
}
|
||||
|
||||
async enableDisableExtension(enable, name) {
|
||||
if (!enable) {
|
||||
const extension = this.extensions.find((e) => e.constructor.name === name);
|
||||
if (extension) {
|
||||
await this.callExtensionMethod('stop', [], [extension]);
|
||||
this.extensions.splice(this.extensions.indexOf(extension), 1);
|
||||
}
|
||||
} else {
|
||||
const Extension = AllExtensions.find((e) => e.name === name);
|
||||
assert(Extension, `Extension '${name}' does not exist`);
|
||||
const extension = new Extension(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus);
|
||||
this.extensions.push(extension);
|
||||
this.callExtensionMethod('onZigbeeStarted', [], [extension]);
|
||||
this.callExtensionMethod('onMQTTConnected', [], [extension]);
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
// Call extensions
|
||||
await this.callExtensionMethod('stop', []);
|
||||
@@ -333,7 +344,7 @@ class Controller {
|
||||
async iteratePayloadAttributeOutput(topicRoot, payload, options) {
|
||||
for (const [key, value] of Object.entries(payload)) {
|
||||
let subPayload = value;
|
||||
let message;
|
||||
let message = null;
|
||||
|
||||
// Special cases
|
||||
if (key === 'color' && utils.objectHasProperties(subPayload, ['r', 'g', 'b'])) {
|
||||
@@ -341,20 +352,24 @@ class Controller {
|
||||
}
|
||||
|
||||
// Check Array first, since it is also an Object
|
||||
if (Array.isArray(subPayload)) {
|
||||
if (subPayload === null || subPayload === undefined) {
|
||||
message = '';
|
||||
} else if (Array.isArray(subPayload)) {
|
||||
message = subPayload.map((x) => `${x}`).join(',');
|
||||
} else if (typeof subPayload === 'object') {
|
||||
return this.iteratePayloadAttributeOutput(`${topicRoot}${key}-`, subPayload, options);
|
||||
this.iteratePayloadAttributeOutput(`${topicRoot}${key}-`, subPayload, options);
|
||||
} else {
|
||||
message = typeof subPayload === 'string' ? subPayload : JSON.stringify(subPayload);
|
||||
}
|
||||
|
||||
await this.mqtt.publish(`${topicRoot}${key}`, message, options);
|
||||
if (message !== null) {
|
||||
await this.mqtt.publish(`${topicRoot}${key}`, message, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async callExtensionMethod(method, parameters) {
|
||||
for (const extension of this.extensions) {
|
||||
async callExtensionMethod(method, parameters, extensions=null) {
|
||||
for (const extension of extensions || this.extensions) {
|
||||
if (extension[method]) {
|
||||
try {
|
||||
await extension[method](...parameters);
|
||||
|
||||
+18
-1
@@ -8,18 +8,35 @@ const allowedEvents = [
|
||||
'publishEntityState', // Entity state will be published
|
||||
'stateChange', // Entity changes its state
|
||||
'groupMembersChanged', // Members of a group has been changed
|
||||
'reportingDisabled', // Reporting is disabled for a device
|
||||
];
|
||||
|
||||
class EventBus extends events.EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this.callbackByExtension = {};
|
||||
}
|
||||
|
||||
emit(event, data) {
|
||||
assert(allowedEvents.includes(event), `Event '${event}' not supported`);
|
||||
super.emit(event, data);
|
||||
}
|
||||
|
||||
on(event, callback) {
|
||||
on(event, callback, extension=null) {
|
||||
assert(allowedEvents.includes(event), `Event '${event}' not supported`);
|
||||
if (extension) {
|
||||
if (!this.callbackByExtension[extension]) this.callbackByExtension[extension] = [];
|
||||
this.callbackByExtension[extension].push({event, callback});
|
||||
}
|
||||
|
||||
super.on(event, callback);
|
||||
}
|
||||
|
||||
removeListenersExtension(extension) {
|
||||
for (const entry of this.callbackByExtension[extension] || []) {
|
||||
super.removeListener(entry.event, entry.callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EventBus;
|
||||
|
||||
@@ -57,7 +57,7 @@ class Availability extends Extension {
|
||||
}
|
||||
|
||||
// Device is a mains powered router
|
||||
return utils.isRouter(device) && !utils.isBatteryPowered(device);
|
||||
return device.type === 'Router' && device.powerSource !== 'Battery';
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
@@ -124,6 +124,7 @@ class Availability extends Extension {
|
||||
}
|
||||
|
||||
async stop() {
|
||||
super.stop();
|
||||
for (const timer of Object.values(this.timers)) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ const legacyTopicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/(
|
||||
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/(bind|unbind)`);
|
||||
const Extension = require('./extension');
|
||||
|
||||
const clusters = ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl', 'closuresWindowCovering'];
|
||||
const clusterCandidates = ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl', 'closuresWindowCovering'];
|
||||
|
||||
// See zigbee-herdsman-converters devices.js
|
||||
const defaultBindGroup = {type: 'group_number', ID: 901};
|
||||
@@ -34,6 +34,7 @@ class Bind extends Extension {
|
||||
let type = null;
|
||||
let sourceKey = null;
|
||||
let targetKey = null;
|
||||
let clusters = null;
|
||||
|
||||
if (this.legacyApi && topic.match(legacyTopicRegex)) {
|
||||
topic = topic.replace(`${settings.get().mqtt.base_topic}/bridge/`, '');
|
||||
@@ -45,13 +46,14 @@ class Bind extends Extension {
|
||||
message = JSON.parse(message);
|
||||
sourceKey = message.from;
|
||||
targetKey = message.to;
|
||||
clusters = message.clusters;
|
||||
}
|
||||
|
||||
return {type, sourceKey, targetKey};
|
||||
return {type, sourceKey, targetKey, clusters};
|
||||
}
|
||||
|
||||
async onMQTTMessage(topic, message) {
|
||||
const {type, sourceKey, targetKey} = this.parseMQTTMessage(topic, message);
|
||||
const {type, sourceKey, targetKey, clusters} = this.parseMQTTMessage(topic, message);
|
||||
if (!type) return null;
|
||||
|
||||
let error = null;
|
||||
@@ -72,7 +74,9 @@ class Bind extends Extension {
|
||||
|
||||
// Find which clusters are supported by both the source and target.
|
||||
// Groups are assumed to support all clusters.
|
||||
for (const cluster of clusters) {
|
||||
for (const cluster of clusterCandidates) {
|
||||
if (clusters && !clusters.includes(cluster)) continue;
|
||||
|
||||
const targetValid = target.type === 'group' || target.type === 'group_number' ||
|
||||
target.device.type === 'Coordinator' || target.endpoint.supportsInputCluster(cluster);
|
||||
|
||||
|
||||
+31
-1
@@ -3,13 +3,16 @@ const utils = require('../util/utils');
|
||||
const Extension = require('./extension');
|
||||
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
const settings = require('../util/settings');
|
||||
const Transport = require('winston-transport');
|
||||
|
||||
const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`);
|
||||
|
||||
class Bridge extends Extension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
this.enableDisableExtension = enableDisableExtension;
|
||||
this.lastJoinedDeviceIeeeAddr = null;
|
||||
this.setupMQTTLogging();
|
||||
|
||||
this.requestLookup = {
|
||||
'device/options': this.deviceOptions.bind(this),
|
||||
@@ -21,6 +24,7 @@ class Bridge extends Extension {
|
||||
'group/rename': this.groupRename.bind(this),
|
||||
'permit_join': this.permitJoin.bind(this),
|
||||
'config/last_seen': this.configLastSeen.bind(this),
|
||||
'config/homeassistant': this.configHomeAssistant.bind(this),
|
||||
'config/elapsed': this.configElapsed.bind(this),
|
||||
'config/log_level': this.configLogLevel.bind(this),
|
||||
'touchlink/factory_reset': this.touchlinkFactoryReset.bind(this),
|
||||
@@ -39,6 +43,19 @@ class Bridge extends Extension {
|
||||
await this.publishGroups();
|
||||
}
|
||||
|
||||
setupMQTTLogging() {
|
||||
const mqtt = this.mqtt;
|
||||
class EventTransport extends Transport {
|
||||
log(info, callback) {
|
||||
const payload = JSON.stringify({message: info.message, level: info.level});
|
||||
mqtt.publish(`bridge/logging`, payload, {}, settings.get().mqtt.base_topic, true);
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
logger.add(new EventTransport());
|
||||
}
|
||||
|
||||
async onMQTTMessage(topic, message) {
|
||||
const match = topic.match(requestRegex);
|
||||
if (match && this.requestLookup[match[1].toLowerCase()]) {
|
||||
@@ -155,6 +172,18 @@ class Bridge extends Extension {
|
||||
return utils.getResponse(message, {value}, null);
|
||||
}
|
||||
|
||||
configHomeAssistant(message) {
|
||||
const allowed = [true, false];
|
||||
const value = this.getValue(message);
|
||||
if (!allowed.includes(value)) {
|
||||
throw new Error(`'${value}' is not an allowed value, allowed: ${allowed}`);
|
||||
}
|
||||
|
||||
this.enableDisableExtension(value, 'HomeAssistant');
|
||||
settings.set(['homeassistant'], value);
|
||||
return utils.getResponse(message, {value}, null);
|
||||
}
|
||||
|
||||
configElapsed(message) {
|
||||
const allowed = [true, false];
|
||||
const value = this.getValue(message);
|
||||
@@ -332,6 +361,7 @@ class Bridge extends Extension {
|
||||
version: this.zigbee2mqttVersion.version,
|
||||
commit: this.zigbee2mqttVersion.commitHash,
|
||||
coordinator: this.coordinatorVersion,
|
||||
network: utils.toSnakeCase(await this.zigbee.getNetworkParameters()),
|
||||
log_level: logger.getLevel(),
|
||||
permit_join: await this.zigbee.getPermitJoin(),
|
||||
};
|
||||
|
||||
@@ -11,10 +11,27 @@ class Configure extends Extension {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
|
||||
this.configuring = new Set();
|
||||
this.onReportingDisabled = this.onReportingDisabled.bind(this);
|
||||
this.attempts = {};
|
||||
|
||||
this.topic = `${settings.get().mqtt.base_topic}/bridge/request/device/configure`;
|
||||
this.legacyTopic = `${settings.get().mqtt.base_topic}/bridge/configure`;
|
||||
this.eventBus.on(`reportingDisabled`, this.onReportingDisabled);
|
||||
}
|
||||
|
||||
onReportingDisabled(data) {
|
||||
// Disabling reporting unbinds some cluster which could be bound by configure, re-setup.
|
||||
const device = data.device;
|
||||
|
||||
const resolvedEntity = this.zigbee.resolveEntity(device);
|
||||
if (resolvedEntity.device.meta && resolvedEntity.device.meta.hasOwnProperty('configured')) {
|
||||
delete device.meta.configured;
|
||||
device.save();
|
||||
}
|
||||
|
||||
if (this.shouldConfigure(resolvedEntity)) {
|
||||
this.configure(resolvedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
shouldConfigure(resolvedEntity) {
|
||||
|
||||
@@ -46,7 +46,9 @@ class Extension {
|
||||
/**
|
||||
* Is called once the extension has to stop
|
||||
*/
|
||||
// stop() {}
|
||||
stop() {
|
||||
this.eventBus.removeListenersExtension(this.constructor.name);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Extension;
|
||||
|
||||
+20
-10
@@ -38,13 +38,27 @@ class Groups extends Extension {
|
||||
}
|
||||
|
||||
async onZigbeeStarted() {
|
||||
await this.syncGroupsWithSettings();
|
||||
this.eventBus.on('stateChange', this.onStateChange);
|
||||
await this.syncGroupsWithSettings();
|
||||
}
|
||||
|
||||
async syncGroupsWithSettings() {
|
||||
const settingsGroups = settings.getGroups();
|
||||
const zigbeeGroups = this.zigbee.getGroups();
|
||||
|
||||
const addRemoveFromGroup = async (action, deviceName, groupName, endpoint, group) => {
|
||||
try {
|
||||
logger.info(`${action === 'add' ? 'Adding' : 'Removing'} '${deviceName}' to group '${groupName}'`);
|
||||
if (action === 'remove') {
|
||||
await endpoint.removeFromGroup(group);
|
||||
} else {
|
||||
await endpoint.addToGroup(group);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to ${action} '${deviceName}' from '${groupName}'`);
|
||||
}
|
||||
};
|
||||
|
||||
for (const settingGroup of settingsGroups) {
|
||||
const groupID = settingGroup.ID;
|
||||
const zigbeeGroup = zigbeeGroups.find((g) => g.groupID === groupID) || this.zigbee.createGroup(groupID);
|
||||
@@ -57,28 +71,24 @@ class Groups extends Extension {
|
||||
// In settings but not in zigbee
|
||||
for (const entity of settingsEntity) {
|
||||
if (!zigbeeGroup.hasMember(entity.endpoint)) {
|
||||
logger.info(`Adding '${entity.name}' to group '${settingGroup.friendlyName}'`);
|
||||
await entity.endpoint.addToGroup(zigbeeGroup);
|
||||
addRemoveFromGroup('add', entity.name, settingGroup.friendlyName, entity.endpoint, zigbeeGroup);
|
||||
}
|
||||
}
|
||||
|
||||
// In zigbee but not in settings
|
||||
for (const endpoint of zigbeeGroup.members) {
|
||||
if (!settingsEntity.find((e) => e.endpoint === endpoint)) {
|
||||
const deviceSettings = settings.getDevice(endpoint.getDevice().ieeeAddr);
|
||||
logger.info(`Removing '${deviceSettings.friendlyName}' from group '${settingGroup.friendlyName}'`);
|
||||
await endpoint.removeFromGroup(zigbeeGroup);
|
||||
const deviceName = settings.getDevice(endpoint.getDevice().ieeeAddr).friendlyName;
|
||||
addRemoveFromGroup('remove', deviceName, settingGroup.friendlyName, endpoint, zigbeeGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
for (const zigbeeGroup of zigbeeGroups) {
|
||||
if (!settingsGroups.find((g) => g.ID === zigbeeGroup.groupID)) {
|
||||
for (const endpoint of zigbeeGroup.members) {
|
||||
const deviceSettings = settings.getDevice(endpoint.getDevice().ieeeAddr);
|
||||
logger.info(`Removing '${deviceSettings.friendlyName}' from group ${zigbeeGroup.groupID}`);
|
||||
await endpoint.removeFromGroup(zigbeeGroup);
|
||||
const deviceName = settings.getDevice(endpoint.getDevice().ieeeAddr).friendlyName;
|
||||
addRemoveFromGroup('remove', deviceName, zigbeeGroup.groupID, endpoint, zigbeeGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+157
-58
@@ -27,6 +27,33 @@ const cfg = {
|
||||
value_template: '{{ value_json.sos }}',
|
||||
},
|
||||
},
|
||||
'binary_sensor_alarm': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'alarm',
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.alarm }}',
|
||||
},
|
||||
},
|
||||
'binary_sensor_temperature_alarm': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'temperature_alarm',
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.temperature_alarm }}',
|
||||
},
|
||||
},
|
||||
'binary_sensor_humidity_alarm': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'humidity_alarm',
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.humidity_alarm }}',
|
||||
},
|
||||
},
|
||||
'binary_sensor_presence': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'presence',
|
||||
@@ -116,6 +143,15 @@ const cfg = {
|
||||
device_class: 'battery',
|
||||
},
|
||||
},
|
||||
'binary_sensor_moving': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'moving',
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.moving}}',
|
||||
},
|
||||
},
|
||||
'binary_sensor_update_available': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'update_available',
|
||||
@@ -157,6 +193,14 @@ const cfg = {
|
||||
},
|
||||
|
||||
// Sensor
|
||||
'sensor_update_state': {
|
||||
type: 'sensor',
|
||||
object_id: 'update_available',
|
||||
discovery_payload: {
|
||||
icon: 'mdi:update',
|
||||
value_template: `{{ value_json['update']['state'] }}`,
|
||||
},
|
||||
},
|
||||
'sensor_illuminance': {
|
||||
type: 'sensor',
|
||||
object_id: 'illuminance',
|
||||
@@ -493,6 +537,15 @@ const cfg = {
|
||||
brightness_scale: 254,
|
||||
},
|
||||
},
|
||||
'light_colorhs': {
|
||||
type: 'light',
|
||||
object_id: 'light',
|
||||
discovery_payload: {
|
||||
hs: true,
|
||||
schema: 'json',
|
||||
command_topic: true,
|
||||
},
|
||||
},
|
||||
'light_brightness_colortemp': {
|
||||
type: 'light',
|
||||
object_id: 'light',
|
||||
@@ -758,15 +811,16 @@ const thermostatHeatCool = /* istanbul ignore next */ (minTemp=7, maxTemp=30, te
|
||||
|
||||
// Map Home Assistant configurations to devices.
|
||||
const mapping = {
|
||||
'WXKG01LM': [cfg.sensor_click, cfg.sensor_battery],
|
||||
'WXKG11LM': [cfg.sensor_click, cfg.sensor_battery, cfg.sensor_action],
|
||||
'WXKG12LM': [cfg.sensor_click, cfg.sensor_battery, cfg.sensor_action],
|
||||
// DEPRECATED; BREAKING_IMPROVEMENT: only use sensor_click for WXKG03LM (action hold -> click hold)
|
||||
'WXKG03LM': [cfg.sensor_click, cfg.sensor_battery, cfg.sensor_action],
|
||||
'WXKG01LM': [cfg.sensor_click, cfg.sensor_action, cfg.sensor_battery],
|
||||
'WXKG11LM': [cfg.sensor_click, cfg.sensor_action, cfg.sensor_battery],
|
||||
'WXKG12LM': [cfg.sensor_click, cfg.sensor_action, cfg.sensor_battery],
|
||||
'WXKG03LM': [cfg.sensor_click, cfg.sensor_action, cfg.sensor_battery],
|
||||
'WXKG06LM': [cfg.sensor_battery, cfg.sensor_action],
|
||||
'WXKG02LM': [cfg.sensor_click, cfg.sensor_battery],
|
||||
'WXKG02LM': [cfg.sensor_click, cfg.sensor_action, cfg.sensor_battery],
|
||||
'QBKG04LM': [cfg.switch, cfg.sensor_click, cfg.sensor_action],
|
||||
'QBKG03LM': [switchEndpoint('left'), switchEndpoint('right'), cfg.sensor_click, cfg.sensor_temperature],
|
||||
'QBKG03LM': [
|
||||
switchEndpoint('left'), switchEndpoint('right'), cfg.sensor_click, cfg.sensor_action, cfg.sensor_temperature,
|
||||
],
|
||||
'WSDCGQ01LM': [cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_battery],
|
||||
'WSDCGQ11LM': [cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_pressure, cfg.sensor_battery],
|
||||
'RTCGQ01LM': [cfg.binary_sensor_occupancy, cfg.sensor_battery],
|
||||
@@ -775,7 +829,7 @@ const mapping = {
|
||||
'MCCGQ11LM': [cfg.binary_sensor_contact, cfg.sensor_battery],
|
||||
'SJCGQ11LM': [cfg.binary_sensor_water_leak, cfg.sensor_battery],
|
||||
'MFKZQ01LM': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'ZNCZ02LM': [cfg.switch, cfg.sensor_power, cfg.sensor_temperature, cfg.sensor_consumption],
|
||||
'ZNCZ02LM': [cfg.switch, cfg.sensor_power, cfg.sensor_temperature, cfg.sensor_consumption, cfg.sensor_voltage],
|
||||
'QBCZ11LM': [cfg.switch, cfg.sensor_power],
|
||||
'LED1545G12': [cfg.light_brightness_colortemp],
|
||||
'LED1623G12': [cfg.light_brightness],
|
||||
@@ -841,12 +895,12 @@ const mapping = {
|
||||
'SL 110 W': [cfg.light_brightness],
|
||||
'AE 260': [cfg.light_brightness],
|
||||
'AA68199': [cfg.light_brightness_colortemp],
|
||||
'QBKG11LM': [cfg.switch, cfg.sensor_power, cfg.sensor_click, cfg.sensor_temperature],
|
||||
'QBKG11LM': [cfg.switch, cfg.sensor_power, cfg.sensor_click, cfg.sensor_action, cfg.sensor_temperature],
|
||||
'QBKG21LM': [cfg.switch, cfg.sensor_click, cfg.sensor_action],
|
||||
'QBKG22LM': [cfg.switch, cfg.sensor_click, cfg.sensor_temperature],
|
||||
'QBKG22LM': [switchEndpoint('left'), switchEndpoint('right'), cfg.sensor_action, cfg.sensor_click],
|
||||
'QBKG12LM': [
|
||||
switchEndpoint('left'), switchEndpoint('right'), cfg.sensor_power, cfg.sensor_click,
|
||||
cfg.sensor_temperature,
|
||||
cfg.sensor_temperature, cfg.sensor_action,
|
||||
],
|
||||
'K2RGBW01': [cfg.light_brightness_colortemp_colorxy],
|
||||
'9290011370': [cfg.light_brightness],
|
||||
@@ -961,7 +1015,7 @@ const mapping = {
|
||||
'XDD12LM': [cfg.light_brightness_colortemp],
|
||||
'D1821': [cfg.light_brightness_colortemp_colorxy],
|
||||
'ZNCLDJ11LM': [cfg.cover_position, cfg.sensor_cover],
|
||||
'TS0601': [cfg.cover_position],
|
||||
'TS0601_curtain': [cfg.cover_position],
|
||||
'LTFY004': [cfg.light_brightness_colorxy],
|
||||
'GL-S-007Z': [cfg.light_brightness_colortemp_colorxy],
|
||||
'3325-S': [cfg.sensor_temperature, cfg.binary_sensor_occupancy],
|
||||
@@ -994,7 +1048,7 @@ const mapping = {
|
||||
'GL-C-008-2ID': [lightEndpoint('light_brightness_colorxy', 'rgb'),
|
||||
lightEndpoint('light_brightness_colortemp', 'cct')],
|
||||
'GL-C-007S': [cfg.light_brightness_colorxy],
|
||||
'100.424.11': [cfg.light_brightness_colortemp],
|
||||
'NLG-CCT light': [cfg.light_brightness_colortemp],
|
||||
'AC0251100NJ/AC0251700NJ': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'71831': [cfg.light_brightness_colortemp],
|
||||
'404000/404005/404012': [cfg.light_brightness_colortemp_colorxy],
|
||||
@@ -1010,7 +1064,8 @@ const mapping = {
|
||||
'GL-B-007Z': [cfg.light_brightness_colortemp_colorxy],
|
||||
'81809/81813': [cfg.light_brightness_colortemp_colorxy],
|
||||
'4090130P7': [cfg.light_brightness_colortemp_colorxy],
|
||||
'100.110.39': [cfg.light_brightness_colortemp_colorxy],
|
||||
'NLG-RGBW light': [cfg.light_brightness_colortemp_colorxy],
|
||||
'NLG-RGB-TW light': [cfg.light_brightness_colortemp_colorxy],
|
||||
'TI0001': [switchEndpoint('left'), switchEndpoint('right')],
|
||||
'SPZB0001': [thermostat(5, 30, 'current_heating_setpoint', 0.5), cfg.sensor_battery],
|
||||
'HS3CG': [cfg.binary_sensor_gas],
|
||||
@@ -1027,7 +1082,7 @@ const mapping = {
|
||||
'YRD426NRSC': [cfg.lock, cfg.sensor_battery],
|
||||
'BE468': [cfg.lock, cfg.sensor_battery],
|
||||
'YRD246HA20BP': [cfg.lock, cfg.sensor_battery],
|
||||
'E1743': [cfg.sensor_click, cfg.sensor_battery],
|
||||
'E1743': [cfg.sensor_click, cfg.sensor_action, cfg.sensor_battery],
|
||||
'LED1732G11': [cfg.light_brightness_colortemp],
|
||||
'LED1736G9': [cfg.light_brightness_colortemp],
|
||||
'RB 265': [cfg.light_brightness],
|
||||
@@ -1098,8 +1153,8 @@ const mapping = {
|
||||
cfg.sensor_temperature, cfg.binary_sensor_contact,
|
||||
cfg.sensor_battery, cfg.binary_sensor_battery_low,
|
||||
],
|
||||
'IM6001-BTP01': [cfg.sensor_click, cfg.sensor_temperature, cfg.sensor_battery],
|
||||
'AV2010/34': [cfg.sensor_click],
|
||||
'IM6001-BTP01': [cfg.sensor_click, cfg.sensor_action, cfg.sensor_temperature, cfg.sensor_battery],
|
||||
'AV2010/34': [cfg.sensor_click, cfg.sensor_action],
|
||||
'PP-WHT-US': [
|
||||
cfg.switch, cfg.sensor_power,
|
||||
cfg.sensor_current, cfg.sensor_voltage,
|
||||
@@ -1108,8 +1163,8 @@ const mapping = {
|
||||
cfg.binary_sensor_battery_low, cfg.binary_sensor_carbon_monoxide,
|
||||
cfg.binary_sensor_gas,
|
||||
],
|
||||
'HGZB-1S': [cfg.switch, cfg.sensor_click],
|
||||
'HGZB-045': [cfg.switch, cfg.sensor_click],
|
||||
'HGZB-1S': [cfg.switch, cfg.sensor_click, cfg.sensor_action],
|
||||
'HGZB-045': [cfg.switch, cfg.sensor_click, cfg.sensor_action],
|
||||
'HGZB-43': [switchEndpoint('top'), switchEndpoint('bottom'), switchEndpoint('center')],
|
||||
'HGZB-01A': [cfg.switch],
|
||||
'HGZB-02A': [cfg.light_brightness],
|
||||
@@ -1129,10 +1184,10 @@ const mapping = {
|
||||
],
|
||||
'F-MLT-US-2': [
|
||||
cfg.sensor_temperature, cfg.binary_sensor_contact,
|
||||
cfg.sensor_battery, cfg.binary_sensor_battery_low,
|
||||
cfg.sensor_battery, cfg.binary_sensor_battery_low, cfg.binary_sensor_moving,
|
||||
],
|
||||
'SWO-KEF1PA': [cfg.sensor_action],
|
||||
'HGZB-02S': [cfg.sensor_click, cfg.switch],
|
||||
'HGZB-02S': [cfg.sensor_click, cfg.sensor_action, cfg.switch],
|
||||
'HGZB-41': [cfg.switch],
|
||||
'ZG9101SAC-HP': [cfg.light_brightness],
|
||||
'RS 122': [cfg.light_brightness],
|
||||
@@ -1189,7 +1244,7 @@ const mapping = {
|
||||
'MCT-340 E': [cfg.binary_sensor_contact, cfg.sensor_temperature, cfg.sensor_battery],
|
||||
'MCT-340 SMA': [cfg.binary_sensor_contact, cfg.sensor_temperature, cfg.sensor_battery],
|
||||
'D1542': [cfg.light_brightness_colortemp],
|
||||
'ZGRC-KEY-013': [cfg.sensor_click],
|
||||
'ZGRC-KEY-013': [cfg.sensor_click, cfg.sensor_action],
|
||||
'ZigUP': [cfg.switch],
|
||||
'YRD256HA20BP': [cfg.sensor_battery, cfg.lock],
|
||||
'SZ-ESW01-AU': [cfg.sensor_power, cfg.switch],
|
||||
@@ -1260,7 +1315,7 @@ const mapping = {
|
||||
'AC08559': [cfg.light_brightness_colortemp_colorxy],
|
||||
'LVS-ZB15S': [cfg.switch],
|
||||
'LZL4BWHL01': [cfg.sensor_action],
|
||||
'2AJZ4KPKEY': [cfg.sensor_click, cfg.sensor_battery],
|
||||
'2AJZ4KPKEY': [cfg.sensor_click, cfg.sensor_action, cfg.sensor_battery],
|
||||
'2AJZ4KPFT': [cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_battery],
|
||||
'TT001ZAV20': [cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_battery],
|
||||
'TS0002': [switchEndpoint('l1'), switchEndpoint('l2')],
|
||||
@@ -1287,7 +1342,7 @@ const mapping = {
|
||||
sensorEndpoint('l4'), sensorEndpoint('l5'), sensorEndpoint('l6'),
|
||||
sensorEndpoint('l7'), sensorEndpoint('l8'),
|
||||
cfg.sensor_click, cfg.sensor_temperature, cfg.sensor_voltage,
|
||||
cfg.sensor_pressure, cfg.sensor_humidity,
|
||||
cfg.sensor_pressure, cfg.sensor_humidity, cfg.sensor_action,
|
||||
],
|
||||
'DIYRuZ_R4_5': [
|
||||
switchEndpoint('bottom_left'), switchEndpoint('bottom_right'), switchEndpoint('top_left'),
|
||||
@@ -1308,7 +1363,7 @@ const mapping = {
|
||||
'PSB19-SW27': [cfg.light_brightness],
|
||||
'S1': [cfg.switch, cfg.sensor_power],
|
||||
'S2': [switchEndpoint('l1'), switchEndpoint('l2'), cfg.sensor_power],
|
||||
'ZWallRemote0': [cfg.sensor_click],
|
||||
'ZWallRemote0': [cfg.sensor_click, cfg.sensor_action],
|
||||
'D1': [cfg.light_brightness, cfg.sensor_power],
|
||||
'J1': [cfg.cover_position_tilt, cfg.sensor_power],
|
||||
'73741': [cfg.light_brightness_colortemp_colorxy],
|
||||
@@ -1318,11 +1373,12 @@ const mapping = {
|
||||
'17436/30/P7': [cfg.light_brightness],
|
||||
'17435/30/P7': [cfg.light_brightness_colorxy],
|
||||
'9290018187B': [cfg.light_brightness_colortemp_colorxy],
|
||||
'1746330P7': [cfg.light_brightness_colortemp_colorxy],
|
||||
'1741830P7': [cfg.light_brightness_colortemp_colorxy],
|
||||
'Z3-1BRL': [cfg.sensor_action, cfg.sensor_brightness],
|
||||
'HS1CG-E': [cfg.binary_sensor_gas],
|
||||
'LED1842G3': [cfg.light_brightness],
|
||||
'ROB_200-008-0': [cfg.sensor_battery, cfg.sensor_click],
|
||||
'ROB_200-008-0': [cfg.sensor_battery, cfg.sensor_click, cfg.sensor_action],
|
||||
'ICZB-IW11SW': [cfg.switch],
|
||||
'HV-GUCXZB5': [cfg.light_brightness_colortemp],
|
||||
'HGZB-20A': [cfg.switch],
|
||||
@@ -1332,14 +1388,14 @@ const mapping = {
|
||||
'2AJZ4KPDR': [cfg.sensor_battery, cfg.binary_sensor_contact, cfg.binary_sensor_battery_low],
|
||||
'6717-84': [cfg.switch],
|
||||
'ICZB-KPD18S': [cfg.sensor_battery, cfg.sensor_click, cfg.sensor_action],
|
||||
'8195-55': [cfg.light_brightness_colortemp],
|
||||
'NLG-TW light': [cfg.light_brightness_colortemp],
|
||||
'E1757': [cfg.cover_position, cfg.sensor_battery],
|
||||
'E1926': [cfg.cover_position, cfg.sensor_battery],
|
||||
'LWG004': [cfg.light_brightness],
|
||||
'54668161': [cfg.light_brightness_colortemp],
|
||||
'8718699688820': [cfg.light_brightness],
|
||||
'GL-W-001Z': [cfg.switch],
|
||||
'E1766': [cfg.sensor_click, cfg.sensor_battery],
|
||||
'E1766': [cfg.sensor_click, cfg.sensor_battery, cfg.sensor_action],
|
||||
'929001953101': [cfg.light_brightness_colortemp_colorxy],
|
||||
'8718699673147': [cfg.light_brightness],
|
||||
'3300-P': [cfg.sensor_temperature, cfg.binary_sensor_contact, cfg.sensor_battery],
|
||||
@@ -1379,7 +1435,7 @@ const mapping = {
|
||||
'9290022169': [cfg.light_brightness_colortemp],
|
||||
'TERNCY-PP01': [
|
||||
cfg.sensor_temperature, cfg.binary_sensor_occupancy, cfg.sensor_illuminance, cfg.sensor_illuminance_lux,
|
||||
cfg.sensor_click,
|
||||
cfg.sensor_click, cfg.sensor_action,
|
||||
],
|
||||
'CR11S8UZ': [cfg.sensor_action],
|
||||
'RB 148 T': [cfg.light_brightness_colortemp],
|
||||
@@ -1421,15 +1477,15 @@ const mapping = {
|
||||
'GL-D-004ZS': [cfg.light_brightness_colortemp_colorxy],
|
||||
'AC10787': [cfg.light_brightness_colortemp],
|
||||
'F-APP-UK-V2': [cfg.switch, cfg.sensor_power],
|
||||
'S9TSZGB_3': [cfg.sensor_action],
|
||||
'S9TSZGB_1': [cfg.sensor_action],
|
||||
'TS0043': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'TS0041': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'SP-EUC01': [cfg.switch, cfg.sensor_power],
|
||||
'511.012': [cfg.light_brightness],
|
||||
'GL-S-008Z': [cfg.light_brightness_colortemp_colorxy],
|
||||
'TZSW22FW-L4': [switchEndpoint('top'), switchEndpoint('bottom')],
|
||||
'GDKES-01TZXD': [cfg.switch],
|
||||
'GDKES-02TZXD': [switchEndpoint('left'), switchEndpoint('right')],
|
||||
'GDKES-03TZXD': [switchEndpoint('left'), switchEndpoint('center'), switchEndpoint('right')],
|
||||
'TS0011': [cfg.switch],
|
||||
'TS0012': [switchEndpoint('left'), switchEndpoint('right')],
|
||||
'TS0013': [switchEndpoint('left'), switchEndpoint('center'), switchEndpoint('right')],
|
||||
'6735/6736/6737': [cfg.switch, cfg.sensor_action],
|
||||
'4034031P7': [cfg.light_brightness_colortemp],
|
||||
'5900131C5': [cfg.light_brightness_colortemp],
|
||||
@@ -1463,7 +1519,7 @@ const mapping = {
|
||||
'067773': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'067771': [cfg.light_brightness],
|
||||
'064873': [cfg.sensor_action],
|
||||
'K4003C': [cfg.switch, cfg.sensor_action],
|
||||
'K4003C/L4003C/N4003C/NT4003C': [cfg.switch, cfg.sensor_action],
|
||||
'STZB402': [
|
||||
thermostat(5, 30, 'occupied_heating_setpoint', 0.5),
|
||||
cfg.sensor_local_temperature,
|
||||
@@ -1533,7 +1589,7 @@ const mapping = {
|
||||
'1402755': [cfg.light_brightness],
|
||||
'4503848C5': [cfg.light_brightness_colortemp],
|
||||
'500.48': [cfg.light_brightness],
|
||||
'TS0042': [cfg.sensor_action],
|
||||
'TS0042': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'SNZB-04': [cfg.binary_sensor_contact, cfg.sensor_battery],
|
||||
'SNZB-01': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'SNZB-02': [cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_battery],
|
||||
@@ -1541,22 +1597,23 @@ const mapping = {
|
||||
'07046L': [cfg.sensor_action],
|
||||
'07045L': [cfg.binary_sensor_contact, cfg.binary_sensor_tamper, cfg.binary_sensor_battery_low],
|
||||
'3402831P7': [cfg.light_brightness_colortemp],
|
||||
'TERNCY-SD01': [cfg.sensor_click, cfg.sensor_battery],
|
||||
'TERNCY-SD01': [cfg.sensor_click, cfg.sensor_battery, cfg.sensor_action],
|
||||
'07048L': [cfg.switch, cfg.sensor_power],
|
||||
'ICZB-KPD14S': [cfg.sensor_battery, cfg.sensor_click, cfg.sensor_action],
|
||||
'73743': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'C4': [cfg.sensor_action],
|
||||
'GL-D-003ZS': [cfg.light_brightness_colortemp_colorxy],
|
||||
'66492-001': [cfg.lock, cfg.sensor_battery],
|
||||
'798.09': [cfg.light_brightness_colortemp_colorxy],
|
||||
'371000002': [cfg.light_brightness_colortemp_colorxy],
|
||||
'U202DST600ZB': [lightEndpoint('light_brightness', 'l1'), lightEndpoint('light_brightness', 'l2')],
|
||||
'SM10ZW': [cfg.binary_sensor_contact, cfg.sensor_battery],
|
||||
'ICZB-R11D': [cfg.light_brightness],
|
||||
'SW2500ZB': [cfg.switch],
|
||||
'4512703': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'4512721': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'4512702': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'4090331P9': [cfg.light_brightness_colortemp_colorxy],
|
||||
'HS1EB': [cfg.sensor_click],
|
||||
'HS1EB': [cfg.sensor_click, cfg.sensor_action],
|
||||
'HS2SW1A-N': [cfg.switch],
|
||||
'HS2SW2A-N': [switchEndpoint('left'), switchEndpoint('right')],
|
||||
'HS2SW3A-N': [switchEndpoint('left'), switchEndpoint('right'), switchEndpoint('center')],
|
||||
@@ -1704,10 +1761,7 @@ const mapping = {
|
||||
'500.67': [cfg.sensor_action],
|
||||
'E1E-G7F': [cfg.sensor_action],
|
||||
'LH-990F': [cfg.binary_sensor_occupancy, cfg.binary_sensor_battery_low],
|
||||
'QBKG25LM': [
|
||||
switchEndpoint('left'), switchEndpoint('center'), switchEndpoint('right'), cfg.sensor_action,
|
||||
cfg.sensor_power,
|
||||
],
|
||||
'QBKG25LM': [switchEndpoint('left'), switchEndpoint('center'), switchEndpoint('right'), cfg.sensor_action],
|
||||
'QBKG23LM': [cfg.switch, cfg.sensor_power],
|
||||
'QBKG24LM': [switchEndpoint('left'), switchEndpoint('right'), cfg.sensor_power],
|
||||
'WS-USC01': [cfg.switch],
|
||||
@@ -1748,7 +1802,7 @@ const mapping = {
|
||||
'73773': [cfg.light_brightness_colortemp_colorxy],
|
||||
'KMPCIL_RES005': [
|
||||
cfg.sensor_battery, cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_pressure, cfg.sensor_illuminance,
|
||||
cfg.binary_sensor_occupancy, cfg.switch,
|
||||
cfg.sensor_illuminance_lux, cfg.binary_sensor_occupancy, cfg.switch,
|
||||
],
|
||||
'DIYRuZ_R8_8': [
|
||||
switchEndpoint('l1'), switchEndpoint('l2'), switchEndpoint('l3'), switchEndpoint('l4'),
|
||||
@@ -1781,9 +1835,35 @@ const mapping = {
|
||||
'LXN59-2S7LX1.0': [switchEndpoint('left'), switchEndpoint('right')],
|
||||
'X711A': [cfg.switch],
|
||||
'X712A': [switchEndpoint('l1'), switchEndpoint('l2')],
|
||||
'TYZS1L': [cfg.light_brightness_colortemp_colorxy],
|
||||
'X713A': [switchEndpoint('l1'), switchEndpoint('l2'), switchEndpoint('l3')],
|
||||
'TYZS1L': [cfg.light_colorhs],
|
||||
'43102': [cfg.switch],
|
||||
'1746430P7': [cfg.light_brightness_colortemp_colorxy],
|
||||
'ROB_200-010-0': [cfg.cover_position],
|
||||
'1743830P7': [cfg.light_brightness_colortemp_colorxy],
|
||||
'ZB-CT01': [cfg.light_brightness_colortemp],
|
||||
'8718699703424': [cfg.light_brightness_colortemp_colorxy],
|
||||
'AM25': [cfg.cover_position, cfg.sensor_battery],
|
||||
'43076': [cfg.switch],
|
||||
'ZSTY-SM-11ZG-US-W': [cfg.switch],
|
||||
'ZSTY-SM-1CTZG-US-W': [cfg.cover_position],
|
||||
'ZSTY-SM-1DMZG-US-W': [cfg.light_brightness],
|
||||
'SNTZ009': [cfg.binary_sensor_water_leak],
|
||||
'4058075208414': [cfg.light_brightness_colortemp],
|
||||
'4058075208339': [cfg.light_brightness_colortemp_colorxy],
|
||||
'NAS-AB02B0': [
|
||||
cfg.sensor_temperature, cfg.sensor_humidity, cfg.binary_sensor_humidity_alarm,
|
||||
cfg.binary_sensor_temperature_alarm, cfg.binary_sensor_alarm,
|
||||
],
|
||||
'FL 120 C': [cfg.light_brightness_colortemp_colorxy],
|
||||
'9290022267': [cfg.light_brightness_colortemp],
|
||||
'ZNTGMK11LM': [cfg.light_brightness_colortemp_colorxy],
|
||||
'HLQDQ01LM': [cfg.light_brightness],
|
||||
'TS0502A': [cfg.light_brightness_colortemp],
|
||||
'TS0004': [switchEndpoint('l1'), switchEndpoint('l2'), switchEndpoint('l3'), switchEndpoint('l4')],
|
||||
'U86KWF-ZPSJ': [thermostat(5, 30, 'current_heating_setpoint', 0.5)],
|
||||
'D3-DPWK-TY': [thermostat(5, 30, 'current_heating_setpoint', 0.5)],
|
||||
'100.075.74': [cfg.light_brightness_colortemp_colorxy],
|
||||
};
|
||||
|
||||
const defaultStatusTopic = 'homeassistant/status';
|
||||
@@ -1798,6 +1878,8 @@ class HomeAssistant extends Extension {
|
||||
// A map of all discoverd devices
|
||||
this.discovered = {};
|
||||
this.discoveredTriggers = {};
|
||||
this.legacyApi = settings.get().advanced.legacy_api;
|
||||
this.newApi = settings.get().experimental.new_api;
|
||||
|
||||
if (!settings.get().advanced.cache_state) {
|
||||
logger.warn('In order for HomeAssistant integration to work properly set `cache_state: true');
|
||||
@@ -1810,9 +1892,9 @@ class HomeAssistant extends Extension {
|
||||
this.discoveryTopic = settings.get().advanced.homeassistant_discovery_topic;
|
||||
this.statusTopic = settings.get().advanced.homeassistant_status_topic;
|
||||
|
||||
this.eventBus.on('deviceRemoved', (data) => this.onDeviceRemoved(data.device));
|
||||
this.eventBus.on('publishEntityState', (data) => this.onPublishEntityState(data));
|
||||
this.eventBus.on('deviceRenamed', (data) => this.onDeviceRenamed(data.device));
|
||||
this.eventBus.on('deviceRemoved', (data) => this.onDeviceRemoved(data.device), this.constructor.name);
|
||||
this.eventBus.on('publishEntityState', (data) => this.onPublishEntityState(data), this.constructor.name);
|
||||
this.eventBus.on('deviceRenamed', (data) => this.onDeviceRenamed(data.device), this.constructor.name);
|
||||
|
||||
for (const definition of utils.getExternalConvertersDefinitions(settings)) {
|
||||
if (definition.hasOwnProperty('homeassistant')) {
|
||||
@@ -1835,7 +1917,7 @@ class HomeAssistant extends Extension {
|
||||
|
||||
async onPublishEntityState(data) {
|
||||
/**
|
||||
* In case we deal with a lightEndpoint configuration Zigbee2mqtt publishes
|
||||
* In case we deal with a lightEndpoint configuration Zigbee2MQTT publishes
|
||||
* e.g. {state_l1: ON, brightness_l1: 250} to zigbee2mqtt/mydevice.
|
||||
* As the Home Assistant MQTT JSON light cannot be configured to use state_l1/brightness_l1
|
||||
* as the state variables, the state topic is set to zigbee2mqtt/mydevice/l1.
|
||||
@@ -1881,7 +1963,7 @@ class HomeAssistant extends Extension {
|
||||
* Whenever a device publish an {action: *} we discover an MQTT device trigger sensor
|
||||
* and republish it to zigbee2mqtt/my_devic/action
|
||||
*/
|
||||
const key = ['action', 'click'].find((k) => data.payload.hasOwnProperty(k) && data.payload[k] !== '');
|
||||
const key = ['action', 'click'].find((k) => data.payload[k] && data.payload[k] !== '');
|
||||
if (data.entity.definition && key) {
|
||||
const device = data.entity.device;
|
||||
if (!this.discoveredTriggers[device.ieeeAddr]) {
|
||||
@@ -1914,12 +1996,18 @@ class HomeAssistant extends Extension {
|
||||
* Publish a value for update_available (if not there yet) to prevent Home Assistant generating warnings of
|
||||
* this value not being available.
|
||||
*/
|
||||
const supportsOTA = data.entity.definition && data.entity.definition.hasOwnProperty('ota');
|
||||
const mockedValues = [
|
||||
{
|
||||
property: 'update_available',
|
||||
condition: data.entity.device && data.entity.definition && data.entity.definition.hasOwnProperty('ota'),
|
||||
condition: supportsOTA && this.legacyApi,
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
property: 'update',
|
||||
condition: supportsOTA && this.newApi,
|
||||
value: {state: 'idle'},
|
||||
},
|
||||
{
|
||||
property: 'water_leak',
|
||||
condition: data.entity.device && data.entity.definition && mapping[data.entity.definition.model] &&
|
||||
@@ -1953,12 +2041,22 @@ class HomeAssistant extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
getConfigs(mappedModel) {
|
||||
getConfigs(mappedModel, deviceSettings=null) {
|
||||
let configs = mapping[mappedModel.model].slice();
|
||||
configs.push(cfg.sensor_linkquality);
|
||||
|
||||
if (mappedModel.hasOwnProperty('ota')) {
|
||||
configs.push(cfg.binary_sensor_update_available);
|
||||
if (this.legacyApi) {
|
||||
configs.push(cfg.binary_sensor_update_available);
|
||||
}
|
||||
|
||||
if (this.newApi) {
|
||||
configs.push(cfg.sensor_update_state);
|
||||
}
|
||||
}
|
||||
|
||||
if (deviceSettings && deviceSettings.hasOwnProperty('legacy') && !deviceSettings.legacy) {
|
||||
configs = configs.filter((c) => c !== cfg.sensor_click);
|
||||
}
|
||||
|
||||
if (!settings.get().advanced.homeassistant_legacy_triggers) {
|
||||
@@ -1979,7 +2077,7 @@ class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
const friendlyName = deviceSettings.friendlyName;
|
||||
this.getConfigs(definition).forEach((config) => {
|
||||
this.getConfigs(definition, deviceSettings).forEach((config) => {
|
||||
const topic = this.getDiscoveryTopic(config, device);
|
||||
const payload = {...config.discovery_payload};
|
||||
let stateTopic = `${settings.get().mqtt.base_topic}/${deviceSettings.friendlyName}`;
|
||||
@@ -2007,8 +2105,9 @@ class HomeAssistant extends Extension {
|
||||
|
||||
payload.json_attributes_topic = stateTopic;
|
||||
|
||||
// Set (unique) name
|
||||
payload.name = `${friendlyName}_${config.object_id}`;
|
||||
// Set (unique) name, separate by space if friendlyName contains space.
|
||||
const nameSeparator = friendlyName.includes(' ') ? ' ' : '_';
|
||||
payload.name = `${friendlyName}${nameSeparator}${config.object_id}`;
|
||||
|
||||
// Set unique_id
|
||||
payload.unique_id = `${deviceSettings.ID}_${config.object_id}_${settings.get().mqtt.base_topic}`;
|
||||
@@ -2018,7 +2117,7 @@ class HomeAssistant extends Extension {
|
||||
|
||||
// Set availability payload
|
||||
// When using availability_timeout each device has it's own availability topic.
|
||||
// If not, use the availability topic of zigbee2mqtt.
|
||||
// If not, use the availability topic of Zigbee2MQTT.
|
||||
if (settings.get().advanced.availability_timeout) {
|
||||
payload.availability_topic = `${settings.get().mqtt.base_topic}/${friendlyName}/availability`;
|
||||
} else {
|
||||
@@ -2165,7 +2264,7 @@ class HomeAssistant extends Extension {
|
||||
return {
|
||||
identifiers: [`zigbee2mqtt_${resolvedEntity.settings.ID}`],
|
||||
name: resolvedEntity.settings.friendlyName,
|
||||
sw_version: `Zigbee2mqtt ${zigbee2mqttVersion}`,
|
||||
sw_version: `Zigbee2MQTT ${zigbee2mqttVersion}`,
|
||||
model: `${resolvedEntity.definition.description} (${resolvedEntity.definition.model})`,
|
||||
manufacturer: resolvedEntity.definition.vendor,
|
||||
};
|
||||
|
||||
@@ -378,6 +378,7 @@ class BridgeLegacy extends Extension {
|
||||
version: info.version,
|
||||
commit: info.commitHash,
|
||||
coordinator,
|
||||
network: await this.zigbee.getNetworkParameters(),
|
||||
log_level: logger.getLevel(),
|
||||
permit_join: await this.zigbee.getPermitJoin(),
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ class OnEvent extends Extension {
|
||||
}
|
||||
|
||||
async stop() {
|
||||
super.stop();
|
||||
for (const device of this.zigbee.getClients()) {
|
||||
const resolvedEntity = this.zigbee.resolveEntity(device);
|
||||
this.callOnEvent(resolvedEntity, 'stop', {});
|
||||
|
||||
@@ -28,6 +28,17 @@ class OTAUpdate extends Extension {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/check`);
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/update`);
|
||||
}
|
||||
|
||||
for (const device of this.zigbee.getClients()) {
|
||||
// In case Zigbee2MQTT is restared during an update, progress and remaining values are still in state.
|
||||
// remove them.
|
||||
this.removeProgressAndRemainingFromState(device);
|
||||
}
|
||||
}
|
||||
|
||||
removeProgressAndRemainingFromState(device) {
|
||||
this.state.removeKey(device.ieeeAddr, ['update', 'progress']);
|
||||
this.state.removeKey(device.ieeeAddr, ['update', 'remaining']);
|
||||
}
|
||||
|
||||
async onZigbeeEvent(type, data, resolvedEntity) {
|
||||
@@ -44,7 +55,8 @@ class OTAUpdate extends Extension {
|
||||
|
||||
this.lastChecked[data.device.ieeeAddr] = Date.now();
|
||||
const available = await resolvedEntity.definition.ota.isUpdateAvailable(data.device, logger, data.data);
|
||||
this.publishEntityState(data.device.ieeeAddr, {update_available: available});
|
||||
const payload = this.getEntityPublishPayload(available ? 'available' : 'idle');
|
||||
this.publishEntityState(data.device.ieeeAddr, payload);
|
||||
|
||||
if (available) {
|
||||
const message = `Update available for '${resolvedEntity.settings.friendly_name}'`;
|
||||
@@ -88,6 +100,24 @@ class OTAUpdate extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
getEntityPublishPayload(state, progress=null, remaining=null) {
|
||||
const payload = {};
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (this.legacyApi) {
|
||||
payload.update_available = state === 'available';
|
||||
}
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().experimental.new_api) {
|
||||
payload.update = {state};
|
||||
if (progress !== null) payload.update.progress = progress;
|
||||
if (remaining !== null) payload.update.remaining = Math.round(remaining);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
async onMQTTMessage(topic, message) {
|
||||
if ((!this.legacyApi || !topic.match(legacyTopicRegex)) && !topic.match(topicRegex)) {
|
||||
return null;
|
||||
@@ -147,7 +177,8 @@ class OTAUpdate extends Extension {
|
||||
);
|
||||
}
|
||||
|
||||
this.publishEntityState(resolvedEntity.device.ieeeAddr, {update_available: available});
|
||||
const payload = this.getEntityPublishPayload(available ? 'available' : 'idle');
|
||||
this.publishEntityState(resolvedEntity.device.ieeeAddr, payload);
|
||||
this.lastChecked[resolvedEntity.device.ieeeAddr] = Date.now();
|
||||
responseData.updateAvailable = available;
|
||||
} catch (e) {
|
||||
@@ -184,6 +215,9 @@ class OTAUpdate extends Extension {
|
||||
|
||||
logger.info(msg);
|
||||
|
||||
const payload = this.getEntityPublishPayload('updating', progress, remaining);
|
||||
this.publishEntityState(resolvedEntity.device.ieeeAddr, payload);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const meta = {status: `update_progress`, device: resolvedEntity.name, progress};
|
||||
@@ -198,7 +232,9 @@ class OTAUpdate extends Extension {
|
||||
const msg = `Finished update of '${resolvedEntity.name}'` +
|
||||
(to ? `, from '${fromS}' to '${toS}'` : ``);
|
||||
logger.info(msg);
|
||||
this.publishEntityState(resolvedEntity.device.ieeeAddr, {update_available: false});
|
||||
this.removeProgressAndRemainingFromState(resolvedEntity.device);
|
||||
const payload = this.getEntityPublishPayload('idle');
|
||||
this.publishEntityState(resolvedEntity.device.ieeeAddr, payload);
|
||||
responseData.from = from_ ? utils.toSnakeCase(from_) : null;
|
||||
responseData.to = to ? utils.toSnakeCase(to) : null;
|
||||
|
||||
@@ -210,6 +246,10 @@ class OTAUpdate extends Extension {
|
||||
} catch (e) {
|
||||
error = `Update of '${resolvedEntity.name}' failed (${e.message})`;
|
||||
|
||||
this.removeProgressAndRemainingFromState(resolvedEntity.device);
|
||||
const payload = this.getEntityPublishPayload('available');
|
||||
this.publishEntityState(resolvedEntity.device.ieeeAddr, payload);
|
||||
|
||||
/* istanbul ignore else */
|
||||
if (settings.get().advanced.legacy_api) {
|
||||
const meta = {status: `update_failed`, device: resolvedEntity.name};
|
||||
|
||||
@@ -10,14 +10,14 @@ const topicRegex = new RegExp(`^(.+?)(?:/(${utils.getEndpointNames().join('|')})
|
||||
|
||||
const groupConverters = [
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.light_onoff_brightness,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.light_color,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.light_color_colortemp,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.light_alert,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.ignore_transition,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.cover_position_tilt,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.thermostat_occupied_heating_setpoint,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.tint_scene,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.light_brightness_move,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.light_brightness_step,
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp_move,
|
||||
];
|
||||
|
||||
@@ -102,7 +102,11 @@ class EntityPublish extends Extension {
|
||||
// Convert the MQTT message to a Zigbee message.
|
||||
let json = {};
|
||||
if (topic.hasOwnProperty('attribute') && topic.attribute) {
|
||||
json[topic.attribute] = message;
|
||||
try {
|
||||
json[topic.attribute] = JSON.parse(message);
|
||||
} catch (e) {
|
||||
json[topic.attribute] = message;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
json = JSON.parse(message);
|
||||
|
||||
@@ -92,7 +92,7 @@ class Receive extends Extension {
|
||||
* to handle these messages.
|
||||
*/
|
||||
const hasGroupID = data.hasOwnProperty('groupID') && !!data.groupID;
|
||||
if (utils.isXiaomiDevice(data.device) && utils.isRouter(data.device) && hasGroupID) {
|
||||
if (utils.isXiaomiDevice(data.device) && data.device.type === 'Router' && hasGroupID) {
|
||||
logger.debug('Skipping re-transmitted Xiaomi message');
|
||||
return false;
|
||||
}
|
||||
|
||||
+32
-10
@@ -1,5 +1,6 @@
|
||||
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
const logger = require('../util/logger');
|
||||
const settings = require('../util/settings');
|
||||
const ZNLDP12LM = zigbeeHerdsmanConverters.devices.find((d) => d.model === 'ZNLDP12LM');
|
||||
const utils = require('../util/utils');
|
||||
const Extension = require('./extension');
|
||||
@@ -106,6 +107,7 @@ class Report extends Extension {
|
||||
this.queue = new Set();
|
||||
this.failed = new Set();
|
||||
this.pollDebouncers = {};
|
||||
this.enabled = settings.get().advanced.report;
|
||||
}
|
||||
|
||||
shouldIgnoreClusterForDevice(cluster, definition) {
|
||||
@@ -124,34 +126,46 @@ class Report extends Extension {
|
||||
if (this.queue.has(device.ieeeAddr) || this.failed.has(device.ieeeAddr)) return;
|
||||
this.queue.add(device.ieeeAddr);
|
||||
|
||||
const term1 = this.enabled ? 'Setup' : 'Disable';
|
||||
const term2 = this.enabled ? 'setup' : 'disabled';
|
||||
|
||||
try {
|
||||
for (const ep of device.endpoints) {
|
||||
for (const [cluster, configuration] of Object.entries(clusters)) {
|
||||
if (ep.supportsInputCluster(cluster) && !this.shouldIgnoreClusterForDevice(cluster, definition)) {
|
||||
logger.debug(`Setup reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`);
|
||||
logger.debug(`${term1} reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`);
|
||||
|
||||
const items = [];
|
||||
for (const entry of configuration) {
|
||||
if (!entry.hasOwnProperty('condition') || (await entry.condition(ep))) {
|
||||
items.push({...entry});
|
||||
const toAdd = {...entry};
|
||||
if (!this.enabled) toAdd.maximumReportInterval = 0xFFFF;
|
||||
items.push(toAdd);
|
||||
delete items[items.length - 1].condition;
|
||||
}
|
||||
}
|
||||
|
||||
await ep.bind(cluster, this.coordinatorEndpoint);
|
||||
this.enabled ?
|
||||
await ep.bind(cluster, this.coordinatorEndpoint) :
|
||||
await ep.unbind(cluster, this.coordinatorEndpoint);
|
||||
|
||||
await ep.configureReporting(cluster, items);
|
||||
logger.info(
|
||||
`Successfully setup reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`,
|
||||
`Successfully ${term2} reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
device.meta.reporting = reportKey;
|
||||
if (this.enabled) {
|
||||
device.meta.reporting = reportKey;
|
||||
} else {
|
||||
delete device.meta.reporting;
|
||||
this.eventBus.emit('reportingDisabled', {device});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to setup reporting for '${device.ieeeAddr}' - ${error.stack}`,
|
||||
`Failed to ${term1.toLowerCase()} reporting for '${device.ieeeAddr}' - ${error.stack}`,
|
||||
);
|
||||
|
||||
this.failed.add(device.ieeeAddr);
|
||||
@@ -171,13 +185,21 @@ class Report extends Extension {
|
||||
// Ikea TRADFRI tend to forget their reporting after powered off.
|
||||
// Re-setup reporting.
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/966
|
||||
if (messageType === 'deviceAnnounce' && utils.isIkeaTradfriDevice(device)) return true;
|
||||
if (this.enabled && messageType === 'deviceAnnounce' && utils.isIkeaTradfriDevice(device)) return true;
|
||||
|
||||
if (resolvedEntity.device.interviewing === true) return false;
|
||||
if (device.meta.hasOwnProperty('reporting') && device.meta.reporting === reportKey) return false;
|
||||
if (!utils.isRouter(device) || utils.isBatteryPowered(device)) return false;
|
||||
if (device.type !== 'Router' || device.powerSource === 'Battery') return false;
|
||||
// Gledopto devices don't support reporting.
|
||||
if (devicesNotSupportingReporting.includes(definition) || definition.vendor === 'Gledopto') return false;
|
||||
|
||||
if (this.enabled && device.meta.hasOwnProperty('reporting') && device.meta.reporting === reportKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.enabled && !device.meta.hasOwnProperty('reporting')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+8
-4
@@ -99,17 +99,21 @@ class MQTT extends events.EventEmitter {
|
||||
return this.client && !this.client.reconnecting;
|
||||
}
|
||||
|
||||
async publish(topic, payload, options, base=settings.get().mqtt.base_topic) {
|
||||
async publish(topic, payload, options, base=settings.get().mqtt.base_topic, skipLog=false) {
|
||||
topic = `${base}/${topic}`;
|
||||
options = {qos: 0, retain: false, ...options};
|
||||
|
||||
if (!this.isConnected()) {
|
||||
logger.error(`Not connected to MQTT server!`);
|
||||
logger.error(`Cannot send message: topic: '${topic}', payload: '${payload}`);
|
||||
if (!skipLog) {
|
||||
logger.error(`Not connected to MQTT server!`);
|
||||
logger.error(`Cannot send message: topic: '${topic}', payload: '${payload}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`MQTT publish: topic '${topic}', payload '${payload}'`);
|
||||
if (!skipLog) {
|
||||
logger.info(`MQTT publish: topic '${topic}', payload '${payload}'`);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.client.publish(topic, payload, options, () => resolve());
|
||||
|
||||
@@ -87,6 +87,24 @@ class State {
|
||||
this.eventBus.emit('stateChange', {ID, from: fromState, to: toState, reason});
|
||||
}
|
||||
|
||||
removeKey(ID, path) {
|
||||
if (this.exists(ID)) {
|
||||
let state = this.state[ID];
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
const key = path[i];
|
||||
if (i === path.length - 1) {
|
||||
delete state[key];
|
||||
} else {
|
||||
if (state[key]) {
|
||||
state = state[key];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
remove(ID) {
|
||||
if (this.exists(ID)) {
|
||||
delete this.state[ID];
|
||||
|
||||
+8
-2
@@ -46,7 +46,7 @@ const transportsToUse = [
|
||||
winston.format.printf(/* istanbul ignore next */(info) => {
|
||||
let {timestamp, level, message} = info;
|
||||
level = level === 'warning' ? 'warn' : level;
|
||||
const prefix = colorizer.colorize(level, `zigbee2mqtt:${levelWithCompensatedLength[level]}`);
|
||||
const prefix = colorizer.colorize(level, `Zigbee2MQTT:${levelWithCompensatedLength[level]}`);
|
||||
return `${prefix} ${timestamp.split('.')[0]}: ${message}`;
|
||||
}),
|
||||
),
|
||||
@@ -82,7 +82,13 @@ if (output.includes('file')) {
|
||||
/* istanbul ignore next */
|
||||
if (output.includes('syslog')) {
|
||||
require('winston-syslog').Syslog;
|
||||
const options = {app_name: 'zigbee2mqtt', ...settings.get().advanced.log_syslog};
|
||||
const options = {
|
||||
app_name: 'Zigbee2MQTT',
|
||||
format: winston.format.printf(/* istanbul ignore next */(info) => {
|
||||
return `${info.message}`;
|
||||
}),
|
||||
...settings.get().advanced.log_syslog,
|
||||
};
|
||||
if (options.hasOwnProperty('type')) options.type = options.type.toString();
|
||||
transportsToUse.push(new winston.transports.Syslog(options));
|
||||
}
|
||||
|
||||
+34
-14
@@ -72,7 +72,7 @@ const defaults = {
|
||||
* Home Assistant requires ALL attributes to be present in ALL MQTT messages send by the device.
|
||||
* https://community.home-assistant.io/t/missing-value-with-mqtt-only-last-data-set-is-shown/47070/9
|
||||
*
|
||||
* Therefore zigbee2mqtt BY DEFAULT caches all values and resend it with every message.
|
||||
* Therefore Zigbee2MQTT BY DEFAULT caches all values and resend it with every message.
|
||||
* advanced.cache_state in configuration.yaml allows to configure this.
|
||||
* https://www.zigbee2mqtt.io/configuration/configuration.html
|
||||
*/
|
||||
@@ -333,23 +333,40 @@ function write() {
|
||||
}
|
||||
|
||||
function validate() {
|
||||
const validate = ajv.compile(schema);
|
||||
const valid = validate(_settings);
|
||||
if (_settings.advanced && _settings.advanced.network_key && typeof _settings.advanced.network_key === 'string' &&
|
||||
_settings.advanced.network_key !== 'GENERATE') {
|
||||
throw new Error(`advanced.network_key: should be array or 'GENERATE' (is '${_settings.advanced.network_key}')`);
|
||||
try {
|
||||
get();
|
||||
} catch (error) {
|
||||
if (error.name === 'YAMLException') {
|
||||
return [
|
||||
`Your YAML file: '${error.file}' is invalid ` +
|
||||
`(use https://jsonformatter.org/yaml-validator to find and fix the issue)`,
|
||||
];
|
||||
}
|
||||
|
||||
return [error.message];
|
||||
}
|
||||
|
||||
const postfixes = utils.getEndpointNames();
|
||||
const validate = ajv.compile(schema);
|
||||
validate(_settings);
|
||||
if (validate.errors) {
|
||||
return validate.errors.map((v) => `${v.dataPath.substring(1)} ${v.message}`);
|
||||
}
|
||||
|
||||
const errors = [];
|
||||
if (_settings.advanced && _settings.advanced.network_key && typeof _settings.advanced.network_key === 'string' &&
|
||||
_settings.advanced.network_key !== 'GENERATE') {
|
||||
errors.push(`advanced.network_key: should be array or 'GENERATE' (is '${_settings.advanced.network_key}')`);
|
||||
}
|
||||
|
||||
// Verify that all friendly names are unique
|
||||
const names = [];
|
||||
const postfixes = utils.getEndpointNames();
|
||||
const check = (name) => {
|
||||
if (names.includes(name)) throw new Error(`Duplicate friendly_name '${name}' found`);
|
||||
if (postfixes.includes(name)) throw new Error(`Following friendly_name are not allowed: '${postfixes}'`);
|
||||
if (name.match(/.*\/\d*$/)) throw new Error(`Friendly name cannot end with a "/DIGIT" ('${name}')`);
|
||||
if (names.includes(name)) errors.push(`Duplicate friendly_name '${name}' found`);
|
||||
if (postfixes.includes(name)) errors.push(`Following friendly_name are not allowed: '${postfixes}'`);
|
||||
if (name.match(/.*\/\d*$/)) errors.push(`Friendly name cannot end with a "/DIGIT" ('${name}')`);
|
||||
if (name.includes('#') || name.includes('+')) {
|
||||
throw new Error(`MQTT wildcard (+ and #) not allowed in friendly_name ('${name}')`);
|
||||
errors.push(`MQTT wildcard (+ and #) not allowed in friendly_name ('${name}')`);
|
||||
}
|
||||
names.push(name);
|
||||
};
|
||||
@@ -361,7 +378,7 @@ function validate() {
|
||||
if (settingsWithDefaults.mqtt.version !== 5) {
|
||||
for (const device of Object.values(settingsWithDefaults.devices)) {
|
||||
if (device.retention) {
|
||||
throw new Error('MQTT retention requires protocol version 5');
|
||||
errors.push('MQTT retention requires protocol version 5');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -369,7 +386,7 @@ function validate() {
|
||||
const checkAvailabilityList = (list, type) => {
|
||||
list.forEach((e) => {
|
||||
if (!getEntity(e)) {
|
||||
throw new Error(`Non-existing entity '${e}' specified in '${type}'`);
|
||||
errors.push(`Non-existing entity '${e}' specified in '${type}'`);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -379,7 +396,7 @@ function validate() {
|
||||
checkAvailabilityList(settingsWithDefaults.advanced.availability_blocklist, 'availability_blocklist');
|
||||
checkAvailabilityList(settingsWithDefaults.advanced.availability_passlist, 'availability_passlist');
|
||||
|
||||
return !valid ? validate.errors.map((v) => `${v.dataPath.substring(1)} ${v.message}`) : null;
|
||||
return errors;
|
||||
}
|
||||
|
||||
function read() {
|
||||
@@ -729,5 +746,8 @@ module.exports = {
|
||||
_settings = read();
|
||||
_settingsWithDefaults = objectAssignDeep.noMutate(defaults, get());
|
||||
},
|
||||
_clear: () => {
|
||||
_settings = null;
|
||||
},
|
||||
_getDefaults: () => defaults,
|
||||
};
|
||||
|
||||
+5
-17
@@ -1,4 +1,3 @@
|
||||
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
const equals = require('fast-deep-equal/es6');
|
||||
const humanizeDuration = require('humanize-duration');
|
||||
const data = require('./data');
|
||||
@@ -40,22 +39,10 @@ const endpointNames = [
|
||||
'button_16', 'button_17', 'button_18', 'button_19', 'button_20',
|
||||
];
|
||||
|
||||
function flatten(arr) {
|
||||
return arr.reduce((flat, toFlatten) => {
|
||||
return flat.concat(Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten);
|
||||
}, []);
|
||||
}
|
||||
|
||||
function capitalize(s) {
|
||||
return s[0].toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
const forceEndDevice = flatten(
|
||||
['QBKG03LM', 'QBKG04LM', 'ZNMS13LM', 'ZNMS12LM']
|
||||
.map((model) => zigbeeHerdsmanConverters.devices.find((d) => d.model === model))
|
||||
.filter((definition) => definition.hasOwnProperty('zigbeeModel'))
|
||||
.map((definition) => definition.zigbeeModel));
|
||||
|
||||
async function getZigbee2mqttVersion() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const git = require('git-last-commit');
|
||||
@@ -175,8 +162,11 @@ function* getExternalConvertersDefinitions(settings) {
|
||||
function toSnakeCase(value) {
|
||||
if (typeof value === 'object') {
|
||||
for (const key of Object.keys(value)) {
|
||||
value[toSnakeCase(key)] = value[key];
|
||||
delete value[key];
|
||||
const keySnakeCase = toSnakeCase(key);
|
||||
if (key !== keySnakeCase) {
|
||||
value[keySnakeCase] = value[key];
|
||||
delete value[key];
|
||||
}
|
||||
}
|
||||
return value;
|
||||
} else {
|
||||
@@ -197,8 +187,6 @@ module.exports = {
|
||||
(!device.manufacturerName || !device.manufacturerName.startsWith('Trust'));
|
||||
},
|
||||
isIkeaTradfriDevice: (device) => ikeaTradfriManufacturerID.includes(device.manufacturerID),
|
||||
isRouter: (device) => device.type === 'Router' && !forceEndDevice.includes(device.modelID),
|
||||
isBatteryPowered: (device) => device.powerSource && device.powerSource === 'Battery',
|
||||
formatDate: (date, type) => formatDate(date, type),
|
||||
equalsPartial,
|
||||
getResponse,
|
||||
|
||||
+1
-9
@@ -7,17 +7,9 @@ function read(file) {
|
||||
return yaml.safeLoad(fs.readFileSync(file, 'utf8'));
|
||||
} catch (error) {
|
||||
if (error.name === 'YAMLException') {
|
||||
error.message =
|
||||
`\n\n\n` +
|
||||
`\t====================================================================\n` +
|
||||
`\tYour YAML file '${file}' is invalid\n` +
|
||||
`\tUse e.g. https://jsonformatter.org/yaml-validator to find and fix the issue.\n` +
|
||||
`\t====================================================================\n` +
|
||||
`\n\n` +
|
||||
error.message;
|
||||
error.file = file;
|
||||
}
|
||||
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,10 @@ class Zigbee extends events.EventEmitter {
|
||||
return this.herdsman.getCoordinatorVersion();
|
||||
}
|
||||
|
||||
async getNetworkParameters() {
|
||||
return this.herdsman.getNetworkParameters();
|
||||
}
|
||||
|
||||
async reset(type) {
|
||||
await this.herdsman.reset(type);
|
||||
}
|
||||
|
||||
Generated
+1567
-1424
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zigbee2mqtt",
|
||||
"version": "1.14.2",
|
||||
"version": "1.14.3",
|
||||
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
@@ -47,8 +47,8 @@
|
||||
"semver": "^7.3.2",
|
||||
"winston": "^3.3.3",
|
||||
"winston-syslog": "^2.4.4",
|
||||
"zigbee-herdsman": "0.12.108",
|
||||
"zigbee-herdsman-converters": "12.0.147"
|
||||
"zigbee-herdsman": "0.12.116",
|
||||
"zigbee-herdsman-converters": "12.0.161"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "*",
|
||||
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
#!/bin/bash
|
||||
function zigbee2mqtt-show-short-info {
|
||||
echo "Setup for Zigbee2mqtt bridge."
|
||||
echo "Setup for Zigbee2MQTT bridge."
|
||||
}
|
||||
|
||||
function zigbee2mqtt-show-long-info {
|
||||
echo "This script installs the Zigbee2mqtt bridge"
|
||||
echo "This script installs the Zigbee2MQTT bridge"
|
||||
}
|
||||
|
||||
function zigbee2mqtt-show-copyright-info {
|
||||
@@ -20,7 +20,7 @@ if [ -z "${node}" ]; then #Installing NodeJS if not already installed.
|
||||
apt install -y nodejs
|
||||
fi
|
||||
|
||||
echo "Cloning Zigbee2mqtt git repository"
|
||||
echo "Cloning Zigbee2MQTT git repository"
|
||||
git clone https://github.com/Koenkk/zigbee2mqtt.git /opt/zigbee2mqtt
|
||||
chown -R pi:pi /opt/zigbee2mqtt
|
||||
|
||||
|
||||
@@ -56,6 +56,22 @@ describe('Bind', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('Should bind only specifief clusters', async () => {
|
||||
const device = zigbeeHerdsman.devices.remote;
|
||||
const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1);
|
||||
const endpoint = device.getEndpoint(1);
|
||||
mockClear(device);
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/bind', JSON.stringify({from: 'remote', to: 'bulb_color', clusters: ["genOnOff"]}));
|
||||
await flushPromises();
|
||||
expect(endpoint.bind).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/bind',
|
||||
JSON.stringify({"data":{"from":"remote","to":"bulb_color","clusters":["genOnOff"],"failed":[]},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Should log error when there is nothing to bind', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
|
||||
+93
-4
@@ -5,17 +5,18 @@ const MQTT = require('./stub/mqtt');
|
||||
const settings = require('../lib/util/settings');
|
||||
const Controller = require('../lib/controller');
|
||||
const flushPromises = () => new Promise(setImmediate);
|
||||
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
|
||||
|
||||
const {coordinator, bulb, unsupported} = zigbeeHerdsman.devices;
|
||||
const {coordinator, bulb, unsupported, WXKG11LM} = zigbeeHerdsman.devices;
|
||||
zigbeeHerdsman.returnDevices.push(coordinator.ieeeAddr);
|
||||
zigbeeHerdsman.returnDevices.push(bulb.ieeeAddr);
|
||||
zigbeeHerdsman.returnDevices.push(unsupported.ieeeAddr);
|
||||
zigbeeHerdsman.returnDevices.push(WXKG11LM.ieeeAddr);
|
||||
|
||||
describe('Bridge', () => {
|
||||
let controller;
|
||||
|
||||
beforeEach(async () => {
|
||||
MQTT.mock.reconnecting = false;
|
||||
data.writeDefaultConfiguration();
|
||||
settings._reRead();
|
||||
settings.set(['advanced', 'legacy_api'], false);
|
||||
@@ -23,6 +24,7 @@ describe('Bridge', () => {
|
||||
data.writeDefaultState();
|
||||
logger.info.mockClear();
|
||||
logger.warn.mockClear();
|
||||
logger.setTransportsEnabled(false);
|
||||
MQTT.publish.mockClear();
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
device.removeFromDatabase.mockClear();
|
||||
@@ -36,7 +38,7 @@ describe('Bridge', () => {
|
||||
const version = await require('../lib/util/utils').getZigbee2mqttVersion();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/info',
|
||||
JSON.stringify({"version":version.version,"commit":version.commitHash,"coordinator":{"type":"z-Stack","meta":{"version":1,"revision":20190425}},"log_level":"info","permit_join":false}),
|
||||
JSON.stringify({"version":version.version,"commit":version.commitHash,"coordinator":{"type":"z-Stack","meta":{"version":1,"revision":20190425}},"network":{"channel":15,"pan_id":5674,"extended_pan_id":[0,11,22]},"log_level":"info","permit_join":false}),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
@@ -45,13 +47,40 @@ describe('Bridge', () => {
|
||||
it('Should publish devices on startup', async () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/devices',
|
||||
JSON.stringify([{"ieee_address":"0x000b57fffec6a5b2","type":"Router","network_address":40369,"supported":true,"friendly_name":"bulb","definition":{"model":"LED1545G12","vendor":"IKEA","description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","supports":"on/off, brightness, color temperature"},"power_source":"Mains (single phase)","date_code":null,"interviewing":false,"interview_completed":true},{"ieee_address":"0x0017880104e45518","type":"EndDevice","network_address":6536,"supported":false,"friendly_name":"0x0017880104e45518","definition":null,"power_source":"Battery","date_code":null,"interviewing":false,"interview_completed":true}]),
|
||||
JSON.stringify([{"ieee_address":"0x000b57fffec6a5b2","type":"Router","network_address":40369,"supported":true,"friendly_name":"bulb","definition":{"model":"LED1545G12","vendor":"IKEA","description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","supports":"on/off, brightness, color temperature"},"power_source":"Mains (single phase)","date_code":null,"interviewing":false,"interview_completed":true},{"ieee_address":"0x0017880104e45518","type":"EndDevice","network_address":6536,"supported":false,"friendly_name":"0x0017880104e45518","definition":null,"power_source":"Battery","date_code":null,"interviewing":false,"interview_completed":true},{"ieee_address":"0x0017880104e45520","type":"EndDevice","network_address":6537,"supported":true,"friendly_name":"button","definition":{"model":"WXKG11LM","vendor":"Xiaomi","description":"Aqara wireless switch","supports":"single, double click (and triple, quadruple, hold, release depending on model)"},"power_source":"Battery","date_code":null,"interviewing":false,"interview_completed":true}]),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Should log to MQTT', async () => {
|
||||
logger.setTransportsEnabled(true);
|
||||
MQTT.publish.mockClear();
|
||||
logger.info.mockClear();
|
||||
logger.info("this is a test");
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/logging',
|
||||
JSON.stringify({message: 'this is a test', level: 'info'}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(logger.info).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Shouldnt log to MQTT when not connected', async () => {
|
||||
logger.setTransportsEnabled(true);
|
||||
MQTT.mock.reconnecting = true;
|
||||
MQTT.publish.mockClear();
|
||||
logger.info.mockClear();
|
||||
logger.error.mockClear();
|
||||
logger.info("this is a test");
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
expect(logger.info).toHaveBeenCalledTimes(1);
|
||||
expect(logger.error).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should publish devices on startup', async () => {
|
||||
logger.setTransportsEnabled(true);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/groups',
|
||||
JSON.stringify([{"id":1,"friendly_name":"group_1","members":[]},{"id":15071,"friendly_name":"group_tradfri_remote","members":[]},{"id":99,"friendly_name":99,"members":[]},{"id":11,"friendly_name":"group_with_tradfri","members":[]},{"id":2,"friendly_name":"group_2","members":[]}]),
|
||||
@@ -503,6 +532,66 @@ describe('Bridge', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('Should allow to enable/disable Home Assistant extension', async () => {
|
||||
// Test if disabled intially
|
||||
const device = zigbeeHerdsman.devices.WXKG11LM;
|
||||
settings.set(['devices', device.ieeeAddr, 'legacy'], false);
|
||||
const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
expect(settings.get().homeassistant).toBeFalsy();
|
||||
expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function));
|
||||
|
||||
// Disable when already disabled should go OK
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/config/homeassistant', JSON.stringify({value: false}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/config/homeassistant',
|
||||
JSON.stringify({"data":{"value":false},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
);
|
||||
expect(settings.get().homeassistant).toBeFalsy();
|
||||
|
||||
// Enable
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/config/homeassistant', JSON.stringify({value: true}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/config/homeassistant',
|
||||
JSON.stringify({"data":{"value":true},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
);
|
||||
expect(settings.get().homeassistant).toBeTruthy();
|
||||
MQTT.publish.mockClear();
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function));
|
||||
|
||||
// Disable
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/config/homeassistant', JSON.stringify({value: false}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/config/homeassistant',
|
||||
JSON.stringify({"data":{"value":false},"status":"ok"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
);
|
||||
expect(settings.get().homeassistant).toBeFalsy();
|
||||
MQTT.publish.mockClear();
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).not.toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should fail to set Home Assistant when invalid type', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/config/homeassistant', 'invalid_one');
|
||||
await flushPromises();
|
||||
expect(settings.get().homeassistant).toBeFalsy();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/config/homeassistant',
|
||||
JSON.stringify({"data":{},"status":"error","error":"'invalid_one' is not an allowed value, allowed: true,false"}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Should allow to set last_seen', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/config/last_seen', 'ISO_8601');
|
||||
|
||||
@@ -68,6 +68,16 @@ describe('Configure', () => {
|
||||
expectRemoteConfigured();
|
||||
});
|
||||
|
||||
it('Should reconfigure reporting on reportingDisabled event', async () => {
|
||||
expectRemoteConfigured();
|
||||
const device = zigbeeHerdsman.devices.remote;
|
||||
mockClear(device);
|
||||
expectRemoteNotConfigured();
|
||||
controller.eventBus.emit('reportingDisabled', {device})
|
||||
await flushPromises();
|
||||
expectRemoteConfigured();
|
||||
});
|
||||
|
||||
it('Should not configure twice', async () => {
|
||||
expectRemoteConfigured();
|
||||
const device = zigbeeHerdsman.devices.remote;
|
||||
|
||||
+3
-10
@@ -204,15 +204,6 @@ describe('Controller', () => {
|
||||
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('Refuse to start when configuration.yaml is invalid', async () => {
|
||||
settings.set(['permit_join'], 'invalid');
|
||||
await controller.start();
|
||||
expect(logger.error).toHaveBeenCalledWith('Refusing to start, configuration.yaml is not valid, found the following errors:');
|
||||
expect(logger.error).toHaveBeenCalledWith('\t - permit_join should be boolean');
|
||||
expect(mockExit).toHaveBeenCalledTimes(1);
|
||||
expect(mockExit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('Start controller with disable_led', async () => {
|
||||
settings.set(['serial', 'disable_led'], true);
|
||||
await controller.start();
|
||||
@@ -439,7 +430,7 @@ describe('Controller', () => {
|
||||
await controller.start();
|
||||
settings.set(['experimental', 'output'], 'attribute');
|
||||
MQTT.publish.mockClear();
|
||||
await controller.publishEntityState('bulb', {state: 'ON', brightness: 50, color_temp: 370, color: {r: 100, g: 50, b: 10}, dummy: {1: 'yes', 2: 'no'}});
|
||||
await controller.publishEntityState('bulb', {dummy: {1: 'yes', 2: 'no'}, color: {r: 100, g: 50, b: 10}, state: 'ON', test: undefined, test1: null, color_temp: 370, brightness: 50});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/brightness", "50", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
@@ -447,6 +438,8 @@ describe('Controller', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/color", '100,50,10', {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/dummy-1", 'yes', {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/dummy-2", 'no', {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/test1", '', {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/test", '', {"qos": 0, "retain": true}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Publish entity state attribute_json output', async () => {
|
||||
|
||||
@@ -46,6 +46,19 @@ describe('Groups', () => {
|
||||
expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('Apply group updates remove handle fail', async () => {
|
||||
const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1);
|
||||
endpoint.removeFromGroup.mockImplementationOnce(() => {throw new Error("failed!")});
|
||||
const group = zigbeeHerdsman.groups.group_1;
|
||||
group.members.push(endpoint);
|
||||
settings.set(['groups'], {'1': {friendly_name: 'group_1', retain: false,}});
|
||||
logger.error.mockClear();
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
expect(logger.error).toHaveBeenCalledWith(`Failed to remove 'bulb_color' from 'group_1'`);
|
||||
expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([endpoint]);
|
||||
});
|
||||
|
||||
it('Move to non existing group', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
|
||||
+45
-25
@@ -12,7 +12,7 @@ const HomeAssistant = require('../lib/extension/homeassistant');
|
||||
describe('HomeAssistant extension', () => {
|
||||
beforeEach(async () => {
|
||||
this.version = await require('../lib/util/utils').getZigbee2mqttVersion();
|
||||
this.version = `Zigbee2mqtt ${this.version.version}`;
|
||||
this.version = `Zigbee2MQTT ${this.version.version}`;
|
||||
jest.useRealTimers();
|
||||
data.writeDefaultConfiguration();
|
||||
settings._reRead();
|
||||
@@ -433,18 +433,18 @@ describe('HomeAssistant extension', () => {
|
||||
await flushPromises();
|
||||
|
||||
payload = {
|
||||
command_topic: 'zigbee2mqtt/smart_vent/set',
|
||||
position_topic: 'zigbee2mqtt/smart_vent',
|
||||
set_position_topic: 'zigbee2mqtt/smart_vent/set',
|
||||
command_topic: 'zigbee2mqtt/smart vent/set',
|
||||
position_topic: 'zigbee2mqtt/smart vent',
|
||||
set_position_topic: 'zigbee2mqtt/smart vent/set',
|
||||
set_position_template: '{ "position": {{ position }} }',
|
||||
value_template: '{{ value_json.position }}',
|
||||
json_attributes_topic: 'zigbee2mqtt/smart_vent',
|
||||
name: 'smart_vent_cover',
|
||||
json_attributes_topic: 'zigbee2mqtt/smart vent',
|
||||
name: 'smart vent cover',
|
||||
unique_id: '0x0017880104e45551_cover_zigbee2mqtt',
|
||||
device:
|
||||
{
|
||||
identifiers: [ 'zigbee2mqtt_0x0017880104e45551' ],
|
||||
name: 'smart_vent',
|
||||
name: 'smart vent',
|
||||
sw_version: this.version,
|
||||
model: 'Smart vent (SV01)',
|
||||
manufacturer: 'Keen Home'
|
||||
@@ -816,16 +816,17 @@ describe('HomeAssistant extension', () => {
|
||||
MQTT.publish.mockClear();
|
||||
|
||||
const device = zigbeeHerdsman.devices.WXKG11LM;
|
||||
settings.set(['devices', device.ieeeAddr, 'legacy'], false);
|
||||
const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
|
||||
const discoverPayload = {
|
||||
"automation_type":"trigger",
|
||||
"type":"click",
|
||||
"type":"action",
|
||||
"subtype":"single",
|
||||
"payload":"single",
|
||||
"topic":"zigbee2mqtt/button/click",
|
||||
"topic":"zigbee2mqtt/button/action",
|
||||
"device":{
|
||||
"identifiers":[
|
||||
"zigbee2mqtt_0x0017880104e45520"
|
||||
@@ -838,14 +839,14 @@ describe('HomeAssistant extension', () => {
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/device_automation/0x0017880104e45520/click_single/config',
|
||||
'homeassistant/device_automation/0x0017880104e45520/action_single/config',
|
||||
JSON.stringify(discoverPayload),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/button/click',
|
||||
'zigbee2mqtt/button/action',
|
||||
'single',
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function),
|
||||
@@ -853,14 +854,14 @@ describe('HomeAssistant extension', () => {
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/button',
|
||||
JSON.stringify({click: "single", linkquality: 10}),
|
||||
JSON.stringify({action: "single", linkquality: 10}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/button',
|
||||
JSON.stringify({linkquality: 10, click: ""}),
|
||||
JSON.stringify({linkquality: 10, action: ""}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
@@ -870,20 +871,37 @@ describe('HomeAssistant extension', () => {
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).not.toHaveBeenCalledWith(
|
||||
'homeassistant/device_automation/0x0017880104e45520/click_single/config',
|
||||
'homeassistant/device_automation/0x0017880104e45520/action_single/config',
|
||||
JSON.stringify(discoverPayload),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/button/click',
|
||||
'zigbee2mqtt/button/action',
|
||||
'single',
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should not discover sensor_click when legacy: false is set', async () => {
|
||||
settings.set(['devices', '0x0017880104e45520'], {
|
||||
legacy: false,
|
||||
friendly_name: 'weather_sensor',
|
||||
retain: false,
|
||||
})
|
||||
controller = new Controller(false);
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
|
||||
const discovered = MQTT.publish.mock.calls.filter((c) => c[0].includes('0x0017880104e45520')).map((c) => c[0]);
|
||||
expect(discovered.length).toBe(3);
|
||||
expect(discovered).toContain('homeassistant/sensor/0x0017880104e45520/action/config');
|
||||
expect(discovered).toContain('homeassistant/sensor/0x0017880104e45520/battery/config');
|
||||
expect(discovered).toContain('homeassistant/sensor/0x0017880104e45520/linkquality/config');
|
||||
});
|
||||
|
||||
it('Should disable Home Assistant legacy triggers', async () => {
|
||||
settings.set(['advanced', 'homeassistant_legacy_triggers'], false);
|
||||
controller = new Controller(false);
|
||||
@@ -898,16 +916,17 @@ describe('HomeAssistant extension', () => {
|
||||
MQTT.publish.mockClear();
|
||||
|
||||
const device = zigbeeHerdsman.devices.WXKG11LM;
|
||||
settings.set(['devices', device.ieeeAddr, 'legacy'], false);
|
||||
const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
|
||||
const discoverPayload = {
|
||||
"automation_type":"trigger",
|
||||
"type":"click",
|
||||
"type":"action",
|
||||
"subtype":"single",
|
||||
"payload":"single",
|
||||
"topic":"zigbee2mqtt/button/click",
|
||||
"topic":"zigbee2mqtt/button/action",
|
||||
"device":{
|
||||
"identifiers":[
|
||||
"zigbee2mqtt_0x0017880104e45520"
|
||||
@@ -920,14 +939,14 @@ describe('HomeAssistant extension', () => {
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/device_automation/0x0017880104e45520/click_single/config',
|
||||
'homeassistant/device_automation/0x0017880104e45520/action_single/config',
|
||||
JSON.stringify(discoverPayload),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/button/click',
|
||||
'zigbee2mqtt/button/action',
|
||||
'single',
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function),
|
||||
@@ -935,7 +954,7 @@ describe('HomeAssistant extension', () => {
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/button',
|
||||
JSON.stringify({click: "single", linkquality: 10}),
|
||||
JSON.stringify({action: "single", linkquality: 10}),
|
||||
{ retain: false, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
@@ -968,25 +987,26 @@ describe('HomeAssistant extension', () => {
|
||||
expect(logger.error).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should counter an action/click payload with an empty payload', async () => {
|
||||
it('Should counter an action payload with an empty payload', async () => {
|
||||
controller = new Controller(false);
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
MQTT.publish.mockClear();
|
||||
const device = zigbeeHerdsman.devices.WXKG11LM;
|
||||
settings.set(['devices', device.ieeeAddr, 'legacy'], false);
|
||||
const data = {onOff: 1}
|
||||
const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(4);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'single', linkquality: 10});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({action: 'single', linkquality: 10});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/button');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({click: '', linkquality: 10});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({action: '', linkquality: 10});
|
||||
expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('homeassistant/device_automation/0x0017880104e45520/click_single/config');
|
||||
expect(MQTT.publish.mock.calls[3][0]).toStrictEqual('zigbee2mqtt/button/click');
|
||||
expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('homeassistant/device_automation/0x0017880104e45520/action_single/config');
|
||||
expect(MQTT.publish.mock.calls[3][0]).toStrictEqual('zigbee2mqtt/button/action');
|
||||
});
|
||||
|
||||
it('Load Home Assistant mapping from external converters', async () => {
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('Bridge legacy', () => {
|
||||
it('Should publish bridge configuration on startup', async () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/config',
|
||||
JSON.stringify({"version":this.version.version,"commit":this.version.commitHash,"coordinator":{"type":"z-Stack","meta":{"version":1, "revision": 20190425}},"log_level":'info',"permit_join":false}),
|
||||
JSON.stringify({"version":this.version.version,"commit":this.version.commitHash,"coordinator":{"type":"z-Stack","meta":{"version":1, "revision": 20190425}},"network":{"panID":5674,"extendedPanID":[0,11,22],"channel":15},"log_level":'info',"permit_join":false}),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
|
||||
+27
-2
@@ -47,7 +47,7 @@ describe('OTA update', () => {
|
||||
device.save.mockClear();
|
||||
mapped.ota.updateToLatest.mockImplementationOnce((a, b, onUpdate) => {
|
||||
onUpdate(0, null);
|
||||
onUpdate(10, 3600);
|
||||
onUpdate(10, 3600.2123);
|
||||
});
|
||||
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/update', 'bulb');
|
||||
@@ -62,6 +62,21 @@ describe('OTA update', () => {
|
||||
expect(device.save).toHaveBeenCalledTimes(1);
|
||||
expect(device.dateCode).toBe('20190102');
|
||||
expect(device.softwareBuildID).toBe(2);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
JSON.stringify({"update_available":false,"update":{"state":"updating","progress":0}}),
|
||||
{retain: true, qos: 0}, expect.any(Function)
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
JSON.stringify({"update_available":false,"update":{"state":"updating","progress":10,"remaining":3600}}),
|
||||
{retain: true, qos: 0}, expect.any(Function)
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
JSON.stringify({"update_available":false,"update":{"state":"idle"}}),
|
||||
{retain: true, qos: 0}, expect.any(Function)
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/ota_update/update',
|
||||
JSON.stringify({"data":{"id": "bulb","from":{"software_build_id":1,"date_code":"20190101"},"to":{"software_build_id":2,"date_code":"20190102"}},"status":"ok"}),
|
||||
@@ -82,6 +97,11 @@ describe('OTA update', () => {
|
||||
|
||||
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/update', JSON.stringify({id: "bulb"}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
JSON.stringify({"update_available":true,"update":{"state":"available"}}),
|
||||
{retain: true, qos: 0}, expect.any(Function)
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/response/device/ota_update/update',
|
||||
JSON.stringify({"data":{"id": "bulb"},"status":"error","error":"Update of 'bulb' failed (Update failed)"}),
|
||||
@@ -222,7 +242,12 @@ describe('OTA update', () => {
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(false);
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(logger.info).not.toHaveBeenCalledWith(`Update available for 'bulb'`)
|
||||
expect(logger.info).not.toHaveBeenCalledWith(`Update available for 'bulb'`);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bulb',
|
||||
JSON.stringify({"update_available":true,"update":{"state":"available"}}),
|
||||
{retain: true, qos: 0}, expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Should respond with NO_IMAGE_AVAILABLE when not supporting OTA', async () => {
|
||||
|
||||
+59
-6
@@ -351,7 +351,7 @@ describe('Publish', () => {
|
||||
expect(group.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 24248, colory: 18350, transtime: 0}, {});
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.37, y: 0.28}});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.37, y: 0.28}, color_temp: 249});
|
||||
});
|
||||
|
||||
it('Should publish messages to groups color temperature', async () => {
|
||||
@@ -362,7 +362,7 @@ describe('Publish', () => {
|
||||
expect(group.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColorTemp", {colortemp: 100, transtime: 0}, {});
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 100});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.280632719756407, y: 0.288286029784579}, color_temp: 100});
|
||||
});
|
||||
|
||||
it('Should create and publish to group which is in configuration.yaml but not in zigbee-herdsman', async () => {
|
||||
@@ -524,6 +524,25 @@ describe('Publish', () => {
|
||||
expectNothingPublished();
|
||||
});
|
||||
|
||||
it('Should allow to invert cover', async () => {
|
||||
const device = zigbeeHerdsman.devices.J1;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
|
||||
// Non-inverted (open = 100, close = 0)
|
||||
await MQTT.events.message('zigbee2mqtt/J1_cover/set', JSON.stringify({position: 90}));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command).toHaveBeenCalledWith("closuresWindowCovering", "goToLiftPercentage", {percentageliftvalue: 10}, {});
|
||||
|
||||
// // Inverted
|
||||
endpoint.command.mockClear();
|
||||
settings.set(['devices', device.ieeeAddr, 'invert_cover'], true);
|
||||
await MQTT.events.message('zigbee2mqtt/J1_cover/set', JSON.stringify({position: 90}));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command).toHaveBeenCalledWith("closuresWindowCovering", "goToLiftPercentage", {percentageliftvalue: 90}, {});
|
||||
});
|
||||
|
||||
it('Should send state update on toggle specific endpoint', async () => {
|
||||
const device = zigbeeHerdsman.devices.QBKG03LM;
|
||||
const endpoint = device.getEndpoint(2);
|
||||
@@ -980,15 +999,49 @@ describe('Publish', () => {
|
||||
// Turn bulb off so that the bulb gets a state.
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({state: 'OFF'}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenNthCalledWith(1,
|
||||
'zigbee2mqtt/bulb_color',
|
||||
JSON.stringify({state: 'OFF', brightness: 0}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
);
|
||||
|
||||
// Toggle again, now that we have state it should publish state ON
|
||||
// Toggle again, now that we have state it should publish state ON and retrieve the brightness from the bulb.
|
||||
endpoint.read.mockImplementationOnce((cluster, attrs) => {
|
||||
if (cluster === 'genLevelCtrl' && attrs.includes('currentLevel')) {
|
||||
return {currentLevel: 100};
|
||||
}
|
||||
});
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({state: 'TOGGLE'}));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(3);
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bulb_color');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 0});
|
||||
expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
expect(MQTT.publish).toHaveBeenNthCalledWith(2,
|
||||
'zigbee2mqtt/bulb_color',
|
||||
JSON.stringify({state: 'ON', brightness: 100}),
|
||||
{retain: false, qos: 0}, expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Should restore brightness when state hass brightness 0 and bulb is turned ON', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
endpoint.read.mockImplementationOnce((cluster, attrs) => {
|
||||
if (cluster === 'genLevelCtrl' && attrs.includes('currentLevel')) {
|
||||
return {currentLevel: 100};
|
||||
}
|
||||
});
|
||||
controller.state.state = {[device.ieeeAddr]: {state: "OFF", brightness: 0,}};
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({state: "OFF"}));
|
||||
await flushPromises();
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({state: "ON"}));
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish).toHaveBeenNthCalledWith(1, 'zigbee2mqtt/bulb_color', JSON.stringify({state: 'OFF', brightness: 0}), {retain: false, qos: 0}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenNthCalledWith(2, 'zigbee2mqtt/bulb_color', JSON.stringify({state: 'ON', brightness: 100}), {retain: false, qos: 0}, expect.any(Function));
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(2);
|
||||
expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "off", {}, {}]);
|
||||
expect(endpoint.command.mock.calls[1]).toEqual(["genOnOff", "on", {}, {}]);
|
||||
});
|
||||
|
||||
it('Should publish messages with options disableDefaultResponse', async () => {
|
||||
|
||||
+35
-8
@@ -28,9 +28,7 @@ describe('Receive', () => {
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'single', linkquality: 10});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button', JSON.stringify({action: 'single', click: 'single', linkquality: 10}), {retain: false, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message which uses ep (left)', async () => {
|
||||
@@ -41,7 +39,7 @@ describe('Receive', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'left', linkquality: 10});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'left', action: 'single_left', linkquality: 10});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
});
|
||||
|
||||
@@ -53,7 +51,7 @@ describe('Receive', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'right', linkquality: 10});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'right', action: 'single_right', linkquality: 10});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
});
|
||||
|
||||
@@ -69,6 +67,35 @@ describe('Receive', () => {
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false});
|
||||
});
|
||||
|
||||
it('Should allow to invert cover', async () => {
|
||||
const device = zigbeeHerdsman.devices.J1;
|
||||
|
||||
// Non-inverted (open = 100, close = 0)
|
||||
await zigbeeHerdsman.events.message({data: {currentPositionLiftPercentage: 90, currentPositionTiltPercentage: 80}, cluster: 'closuresWindowCovering', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/J1_cover', JSON.stringify({position: 10, tilt: 20, linkquality: 10}), {retain: false, qos: 0}, expect.any(Function));
|
||||
|
||||
// Inverted
|
||||
MQTT.publish.mockClear();
|
||||
settings.set(['devices', device.ieeeAddr, 'invert_cover'], true);
|
||||
await zigbeeHerdsman.events.message({data: {currentPositionLiftPercentage: 90, currentPositionTiltPercentage: 80}, cluster: 'closuresWindowCovering', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/J1_cover', JSON.stringify({position: 90, tilt: 80, linkquality: 10}), {retain: false, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should allow to disable the legacy integration', async () => {
|
||||
const device = zigbeeHerdsman.devices.WXKG11LM;
|
||||
settings.set(['devices', device.ieeeAddr, 'legacy'], false);
|
||||
const data = {onOff: 1}
|
||||
const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button', JSON.stringify({action: 'single', linkquality: 10}), {retain: false, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Should debounce messages', async () => {
|
||||
jest.useFakeTimers();
|
||||
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
|
||||
@@ -397,7 +424,7 @@ describe('Receive', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/ikea_onoff');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'click': 'brightness_stop', linkquality: 10});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'click': 'brightness_stop', action: 'brightness_stop', linkquality: 10});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
});
|
||||
|
||||
@@ -415,10 +442,10 @@ describe('Receive', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/ikea_onoff');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'click': 'brightness_stop'});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'click': 'brightness_stop', action: 'brightness_stop'});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/ikea_onoff');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toMatchObject({'click': 'brightness_stop'});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toMatchObject({'click': 'brightness_stop', action: 'brightness_stop'});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[1][1]).elapsed).toBe(50);
|
||||
expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
Date.now = oldNow;
|
||||
|
||||
@@ -24,6 +24,9 @@ describe('Report', () => {
|
||||
|
||||
function expectOnOffBrightnessColorReport(endpoint, colorXY) {
|
||||
const coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1);
|
||||
const device = endpoint.getDevice();
|
||||
expect(device.meta.reporting).toBe(1);
|
||||
expect(endpoint.unbind).toHaveBeenCalledTimes(0);
|
||||
expect(endpoint.bind).toHaveBeenCalledTimes(3);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', coordinatorEndpoint);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', coordinatorEndpoint);
|
||||
@@ -38,12 +41,32 @@ describe('Report', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function expectOnOffBrightnessColorReportDisabled(endpoint, colorXY) {
|
||||
const coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1);
|
||||
const device = endpoint.getDevice();
|
||||
expect(device.meta.reporting).toBe(undefined);
|
||||
expect(endpoint.unbind).toHaveBeenCalledTimes(3);
|
||||
expect(endpoint.bind).toHaveBeenCalledTimes(0);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', coordinatorEndpoint);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', coordinatorEndpoint);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith('lightingColorCtrl', coordinatorEndpoint);
|
||||
expect(endpoint.configureReporting).toHaveBeenCalledTimes(3);
|
||||
expect(endpoint.configureReporting).toHaveBeenCalledWith('genOnOff', [{"attribute": "onOff", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 0, "reportableChange": 0}]);
|
||||
expect(endpoint.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [{"attribute": "currentLevel", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 3, "reportableChange": 1}]);
|
||||
if (colorXY) {
|
||||
expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [{"attribute": "colorTemperature", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 3, "reportableChange": 1}, {"attribute": "currentX", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 3, "reportableChange": 1}, {"attribute": "currentY", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 3, "reportableChange": 1}]);
|
||||
} else {
|
||||
expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [{"attribute": "colorTemperature", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 3, "reportableChange": 1}]);
|
||||
}
|
||||
}
|
||||
|
||||
mockClear = (device) => {
|
||||
for (const endpoint of device.endpoints) {
|
||||
endpoint.read.mockClear();
|
||||
endpoint.write.mockClear();
|
||||
endpoint.configureReporting.mockClear();
|
||||
endpoint.bind.mockClear();
|
||||
endpoint.unbind.mockClear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +75,11 @@ describe('Report', () => {
|
||||
settings._reRead();
|
||||
data.writeEmptyState();
|
||||
settings.set(['advanced', 'report'], true);
|
||||
for (const device of Object.values(zigbeeHerdsman.devices)) {
|
||||
mockClear(device);
|
||||
delete device.meta.reporting;
|
||||
}
|
||||
|
||||
controller = new Controller();
|
||||
await controller.start();
|
||||
mocksClear.forEach((m) => m.mockClear());
|
||||
@@ -64,6 +92,31 @@ describe('Report', () => {
|
||||
expectOnOffBrightnessColorReport(endpoint, true);
|
||||
});
|
||||
|
||||
it('Should not configure reporting on startup when disabled', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
mockClear(device);
|
||||
delete device.meta.report;
|
||||
settings.set(['advanced', 'report'], false);
|
||||
controller = new Controller();
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
expect(device.meta.reporting).toBe(undefined);
|
||||
expect(endpoint.bind).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should disable reporting on startup when enabled earlier', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
device.meta.reporting = 1;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
settings.set(['advanced', 'report'], false);
|
||||
mockClear(device);
|
||||
controller = new Controller();
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
expectOnOffBrightnessColorReportDisabled(endpoint, true);
|
||||
});
|
||||
|
||||
it('Should configure reporting when receicing message from device which has not been setup yet', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
|
||||
+56
-37
@@ -10,6 +10,12 @@ const groupsFile = data.joinPath('groups.yaml');
|
||||
const secretFile = data.joinPath('secret.yaml');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const minimalConfig = {
|
||||
permit_join: true,
|
||||
homeassistant: true,
|
||||
mqtt: {base_topic: 'zigbee2mqtt', server: 'localhost'},
|
||||
};
|
||||
|
||||
describe('Settings', () => {
|
||||
const write = (file, json, reread=true) => {
|
||||
fs.writeFileSync(file, yaml.safeDump(json))
|
||||
@@ -505,40 +511,63 @@ describe('Settings', () => {
|
||||
|
||||
it('Should not allow any string values for network_key', () => {
|
||||
write(configurationFile, {
|
||||
...minimalConfig,
|
||||
advanced: {network_key: 'NOT_GENERATE'},
|
||||
});
|
||||
|
||||
settings._reRead();
|
||||
|
||||
expect(() => {
|
||||
settings.validate();
|
||||
}).toThrowError(`advanced.network_key: should be array or 'GENERATE' (is 'NOT_GENERATE')`);
|
||||
const error = `advanced.network_key: should be array or 'GENERATE' (is 'NOT_GENERATE')`;
|
||||
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
|
||||
});
|
||||
|
||||
it('Should allow retention configuration with MQTT v5', () => {
|
||||
write(configurationFile, {
|
||||
...minimalConfig,
|
||||
mqtt: {base_topic: 'zigbee2mqtt', server: 'localhost', version: 5},
|
||||
devices: {'0x0017880104e45519': {friendly_name: 'tain', retention: 900}},
|
||||
});
|
||||
|
||||
settings._reRead();
|
||||
expect(settings.validate()).toEqual([]);
|
||||
});
|
||||
|
||||
|
||||
it('Should not allow retention configuration without MQTT v5', () => {
|
||||
write(configurationFile, {
|
||||
...minimalConfig,
|
||||
devices: {'0x0017880104e45519': {friendly_name: 'tain', retention: 900}},
|
||||
});
|
||||
|
||||
settings._reRead();
|
||||
|
||||
expect(() => {
|
||||
settings.validate();
|
||||
}).toThrowError('MQTT retention requires protocol version 5');
|
||||
const error = 'MQTT retention requires protocol version 5';
|
||||
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
|
||||
});
|
||||
|
||||
it('Should not allow non-existing entities in availability_blocklist', () => {
|
||||
write(configurationFile, {
|
||||
...minimalConfig,
|
||||
devices: {'0x0017880104e45519': {friendly_name: 'tain'}},
|
||||
advanced: {availability_blocklist: ['0x0017880104e45519', 'non_existing']},
|
||||
});
|
||||
|
||||
settings._reRead();
|
||||
|
||||
expect(() => {
|
||||
settings.validate();
|
||||
}).toThrowError(`Non-existing entity 'non_existing' specified in 'availability_blocklist'`);
|
||||
const error = `Non-existing entity 'non_existing' specified in 'availability_blocklist'`;
|
||||
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
|
||||
});
|
||||
|
||||
it('Validate should if settings does not conform to scheme', () => {
|
||||
write(configurationFile, {
|
||||
...minimalConfig,
|
||||
advanced: null,
|
||||
});
|
||||
|
||||
settings._reRead();
|
||||
|
||||
const error = `advanced should be object`;
|
||||
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
|
||||
});
|
||||
|
||||
it('Should ban devices', () => {
|
||||
@@ -563,74 +592,64 @@ describe('Settings', () => {
|
||||
\t wrong
|
||||
`)
|
||||
|
||||
let error;
|
||||
try {
|
||||
settings._reRead();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error.message).toContain(`Your YAML file '${configurationFile}' is invalid`);
|
||||
settings._clear();
|
||||
const error = `Your YAML file: '${configurationFile}' is invalid (use https://jsonformatter.org/yaml-validator to find and fix the issue)`;
|
||||
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
|
||||
});
|
||||
|
||||
it('Should throw error when yaml file does not exist', () => {
|
||||
let error;
|
||||
try {
|
||||
settings._reRead();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error.message).toContain(`no such file or directory, open`);
|
||||
settings._clear();
|
||||
const error = `ENOENT: no such file or directory, open '${configurationFile}'`;
|
||||
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
|
||||
});
|
||||
|
||||
it('Configuration shouldnt be valid when duplicate friendly_name are used', async () => {
|
||||
write(configurationFile, {
|
||||
...minimalConfig,
|
||||
devices: {'0x0017880104e45519': {friendly_name: 'myname', retain: false}},
|
||||
groups: {'1': {friendly_name: 'myname', retain: false}},
|
||||
});
|
||||
|
||||
settings._reRead();
|
||||
|
||||
expect(() => {
|
||||
settings.validate();
|
||||
}).toThrowError(`Duplicate friendly_name 'myname' found`);
|
||||
const error = `Duplicate friendly_name 'myname' found`;
|
||||
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
|
||||
});
|
||||
|
||||
it('Configuration shouldnt be valid when friendly_name ends with /DIGIT', async () => {
|
||||
write(configurationFile, {
|
||||
...minimalConfig,
|
||||
devices: {'0x0017880104e45519': {friendly_name: 'myname/123', retain: false}},
|
||||
});
|
||||
|
||||
settings._reRead();
|
||||
|
||||
expect(() => {
|
||||
settings.validate();
|
||||
}).toThrowError(`Friendly name cannot end with a "/DIGIT" ('myname/123')`);
|
||||
const error = `Friendly name cannot end with a "/DIGIT" ('myname/123')`;
|
||||
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
|
||||
});
|
||||
|
||||
it('Configuration shouldnt be valid when friendly_name contains a MQTT wildcard', async () => {
|
||||
write(configurationFile, {
|
||||
...minimalConfig,
|
||||
devices: {'0x0017880104e45519': {friendly_name: 'myname#', retain: false}},
|
||||
});
|
||||
|
||||
settings._reRead();
|
||||
|
||||
expect(() => {
|
||||
settings.validate();
|
||||
}).toThrowError(`MQTT wildcard (+ and #) not allowed in friendly_name ('myname#')`);
|
||||
const error = `MQTT wildcard (+ and #) not allowed in friendly_name ('myname#')`;
|
||||
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
|
||||
});
|
||||
|
||||
it('Configuration shouldnt be valid when friendly_name is a postfix', async () => {
|
||||
write(configurationFile, {
|
||||
...minimalConfig,
|
||||
devices: {'0x0017880104e45519': {friendly_name: 'left', retain: false}},
|
||||
});
|
||||
|
||||
settings._reRead();
|
||||
|
||||
expect(() => {
|
||||
settings.validate();
|
||||
}).toThrowError(`Following friendly_name are not allowed: '${utils.getEndpointNames()}'`);
|
||||
const error = `Following friendly_name are not allowed: '${utils.getEndpointNames()}'`;
|
||||
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
|
||||
});
|
||||
|
||||
it('Configuration shouldnt be valid when duplicate friendly_name are used', async () => {
|
||||
|
||||
+5
-2
@@ -109,7 +109,7 @@ function writeDefaultConfiguration() {
|
||||
},
|
||||
'0x0017880104e45551': {
|
||||
retain: false,
|
||||
friendly_name: 'smart_vent'
|
||||
friendly_name: 'smart vent'
|
||||
},
|
||||
'0x0017880104e45552': {
|
||||
retain: false,
|
||||
@@ -154,7 +154,10 @@ function writeDefaultConfiguration() {
|
||||
},
|
||||
'0x0017880104e44559': {
|
||||
friendly_name: '3157100_thermostat',
|
||||
}
|
||||
},
|
||||
'0x0017880104a44559': {
|
||||
friendly_name: 'J1_cover',
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
'1': {
|
||||
|
||||
+17
-5
@@ -1,14 +1,26 @@
|
||||
let level = 'info';
|
||||
|
||||
const transports = [];
|
||||
|
||||
let transportsEnabled = false;
|
||||
const callTransports = (level, message) => {
|
||||
if (transportsEnabled) {
|
||||
for (const transport of transports) {
|
||||
transport.log({level, message}, () => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mock = {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
trace: jest.fn(),
|
||||
info: jest.fn().mockImplementation((msg) => callTransports('info', msg)),
|
||||
warn: jest.fn().mockImplementation((msg) => callTransports('warn', msg)),
|
||||
error: jest.fn().mockImplementation((msg) => callTransports('error', msg)),
|
||||
debug: jest.fn().mockImplementation((msg) => callTransports('debug', msg)),
|
||||
cleanup: jest.fn(),
|
||||
add: (transport) => transports.push(transport),
|
||||
setLevel: (newLevel) => {level = newLevel},
|
||||
getLevel: () => level,
|
||||
setTransportsEnabled: (value) => {transportsEnabled = value},
|
||||
};
|
||||
|
||||
jest.mock('../../lib/util/logger', () => (mock));
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@ const mock = {
|
||||
publish: jest.fn().mockImplementation((topic, payload, options, cb) => cb()),
|
||||
end: jest.fn(),
|
||||
subscribe: jest.fn(),
|
||||
reconnecting: false,
|
||||
on: (type, handler) => {
|
||||
if (type === 'connect') {
|
||||
handler();
|
||||
@@ -20,5 +21,5 @@ jest.mock('mqtt', () => {
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
events, ...mock, connect: mockConnect,
|
||||
events, ...mock, connect: mockConnect, mock,
|
||||
};
|
||||
@@ -57,12 +57,13 @@ class Endpoint {
|
||||
return Object.values(devices).find(d => d.ieeeAddr === deviceIeeeAddress);
|
||||
}
|
||||
|
||||
this.removeFromGroup = (group) => {
|
||||
this.removeFromGroup = jest.fn();
|
||||
this.removeFromGroup.mockImplementation((group) => {
|
||||
const index = group.members.indexOf(this);
|
||||
if (index != -1) {
|
||||
group.members.splice(index, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.removeFromAllGroups = () => {
|
||||
Object.values(groups).forEach((g) => this.removeFromGroup(g))
|
||||
@@ -148,6 +149,7 @@ const devices = {
|
||||
'GL-S-007ZS': new Device('Router', '0x0017880104e45526', 6540,4151, [new Endpoint(1, [0], [], '0x0017880104e45526')], true, "Mains (single phase)", 'GL-S-007ZS'),
|
||||
'U202DST600ZB': new Device('Router', '0x0017880104e43559', 6540,4151, [new Endpoint(10, [0, 6], [], '0x0017880104e43559'), new Endpoint(11, [0, 6], [], '0x0017880104e43559')], true, "Mains (single phase)", 'U202DST600ZB'),
|
||||
'3157100': new Device('Router', '0x0017880104e44559', 6542,4151, [new Endpoint(1, [], [], '0x0017880104e44559')], true, "Mains (single phase)", '3157100'),
|
||||
'J1': new Device('Router', '0x0017880104a44559', 6543,4151, [new Endpoint(1, [], [], '0x0017880104a44559')], true, "Mains (single phase)", 'J1 (5502)'),
|
||||
}
|
||||
|
||||
const groups = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
echo "Stopping zigbee2mqtt..."
|
||||
echo "Stopping Zigbee2MQTT..."
|
||||
sudo systemctl stop zigbee2mqtt
|
||||
|
||||
echo "Creating backup of configuration..."
|
||||
@@ -17,7 +17,7 @@ echo "Restore configuration..."
|
||||
cp -R data-backup/* data
|
||||
rm -rf data-backup
|
||||
|
||||
echo "Starting zigbee2mqtt..."
|
||||
echo "Starting Zigbee2MQTT..."
|
||||
sudo systemctl start zigbee2mqtt
|
||||
|
||||
echo "Done!"
|
||||
|
||||
Reference in New Issue
Block a user