mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 07:08:56 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31e56785e4 | ||
|
|
962282b38c | ||
|
|
e241d5ac8a | ||
|
|
1a973d0cd7 | ||
|
|
11ec191580 | ||
|
|
95788b8847 | ||
|
|
a36f11a4fb | ||
|
|
9cc84feb67 | ||
|
|
dd82b29629 | ||
|
|
2a9d0d1875 | ||
|
|
66173ff689 | ||
|
|
645da4e3c0 | ||
|
|
409df4c193 | ||
|
|
aca32e2f02 | ||
|
|
79116a1158 | ||
|
|
ad59ff57bc | ||
|
|
df67c429bf | ||
|
|
6b5b4ab433 | ||
|
|
c25c32f1be | ||
|
|
9b644ff82d | ||
|
|
0289a6c30d | ||
|
|
2b3cdebe41 | ||
|
|
6ef4fac30a | ||
|
|
f0903cd02c | ||
|
|
58d987b523 | ||
|
|
9a87abd0f4 | ||
|
|
b379830e36 | ||
|
|
97af26b340 | ||
|
|
7de51340a0 | ||
|
|
bbf432eae5 | ||
|
|
eac15a748a | ||
|
|
87faaa1817 | ||
|
|
984bad4cb3 | ||
|
|
13b996eb70 | ||
|
|
fb7c3a31dd | ||
|
|
06b6d66090 | ||
|
|
3d897c55e2 | ||
|
|
c5fb651481 | ||
|
|
7d52a013ad | ||
|
|
e278bb0145 | ||
|
|
b36da25d68 | ||
|
|
83291e242e | ||
|
|
99dc214fbe | ||
|
|
5e152f2ce4 | ||
|
|
8df6be8813 | ||
|
|
415e764592 | ||
|
|
e8d9cbc19b | ||
|
|
e473590dd9 | ||
|
|
558153ebd6 | ||
|
|
a07a28cb65 | ||
|
|
6090f124c0 | ||
|
|
5d8a9a3e51 | ||
|
|
313edcc445 | ||
|
|
4926fb85ae | ||
|
|
0111681cb7 | ||
|
|
9bc427e32f | ||
|
|
36e39e2f02 | ||
|
|
58e08d1443 | ||
|
|
2e19a8f916 | ||
|
|
5488504809 | ||
|
|
82fb79820a | ||
|
|
c87ccea9ba | ||
|
|
b6cb7acf7e | ||
|
|
ad286cbbc6 | ||
|
|
fe62ceb0f9 | ||
|
|
61a3f0e839 | ||
|
|
b58cbd4883 | ||
|
|
833bfcd81a | ||
|
|
9e413bf35a | ||
|
|
3c28ed8f0b | ||
|
|
20ba6a5274 | ||
|
|
bd30b0b4a3 | ||
|
|
93e96a2e5e | ||
|
|
e1d51e6171 | ||
|
|
cb855373ac | ||
|
|
145f8a577d | ||
|
|
0d3fa83ab1 | ||
|
|
dae52a22d7 | ||
|
|
e94b34caad | ||
|
|
43c745df1d | ||
|
|
2e958f863f | ||
|
|
3b1ef5179b | ||
|
|
ae5330d090 | ||
|
|
dd0ca2a392 | ||
|
|
39ed95371e | ||
|
|
3f6a95bc0b | ||
|
|
42f1a37499 | ||
|
|
287f843bbb | ||
|
|
cd4319d281 | ||
|
|
2188937eea | ||
|
|
a9cce7c222 | ||
|
|
f9d85df51b | ||
|
|
64315d0fc9 | ||
|
|
eba84647c1 | ||
|
|
7d268732de | ||
|
|
a79b2bae81 | ||
|
|
5c18734e68 | ||
|
|
2234fd1e8a | ||
|
|
80838bd248 | ||
|
|
4a0059afcd | ||
|
|
4a14706638 | ||
|
|
9890fb3ae2 | ||
|
|
15d984e21b | ||
|
|
57a5253203 | ||
|
|
7a8bef3e60 | ||
|
|
77fc851236 | ||
|
|
e76633519f | ||
|
|
a652df6ed7 | ||
|
|
cb0b0b5af9 | ||
|
|
3a463b0782 | ||
|
|
0df966755c | ||
|
|
94d0253f27 | ||
|
|
4ee34fd637 | ||
|
|
cabb1f3979 | ||
|
|
8a72916d7d | ||
|
|
c041f0cddc | ||
|
|
6245c729cf | ||
|
|
e5b2f71fcd | ||
|
|
d0e9889f7d | ||
|
|
d3aba2689c | ||
|
|
7a6d5f9a42 | ||
|
|
89fb54d5c8 | ||
|
|
afaed3f783 | ||
|
|
dc6bf51b8e | ||
|
|
5f97f19d7f | ||
|
|
f04e01ff54 | ||
|
|
f069a7e58e | ||
|
|
2197d4127d | ||
|
|
89fe701197 | ||
|
|
e63d196966 | ||
|
|
9db1467ed8 | ||
|
|
3bab32c1d7 | ||
|
|
66b3bcb639 | ||
|
|
6f7d29392e | ||
|
|
83e10f446e | ||
|
|
af4184e3ff | ||
|
|
316755a5c8 | ||
|
|
5083bbcc16 | ||
|
|
981612c64c | ||
|
|
960f9e44ad | ||
|
|
8545a341c9 | ||
|
|
da4d26af62 | ||
|
|
f64bf16f6f | ||
|
|
0b9997b7a5 | ||
|
|
6f33529c79 | ||
|
|
1972eb450c | ||
|
|
f4f910f014 | ||
|
|
087d1b8761 | ||
|
|
4334e01759 | ||
|
|
6fd059967a | ||
|
|
61f5776310 | ||
|
|
3f2d50900d | ||
|
|
be601cffb0 | ||
|
|
6b7bd9da37 | ||
|
|
3a9dfb04a7 | ||
|
|
67f8e97b12 | ||
|
|
37089e440a | ||
|
|
d17eec5310 | ||
|
|
bcf79a7e34 | ||
|
|
6bd9e74e0c | ||
|
|
6aad019328 | ||
|
|
d05a3a066a | ||
|
|
5440aadec0 | ||
|
|
be30067a39 | ||
|
|
17560ca885 | ||
|
|
e00dbbf4a9 | ||
|
|
33201367f1 | ||
|
|
60627ab6e3 | ||
|
|
88a1c2884a | ||
|
|
a53a19026c | ||
|
|
1459ffea7a | ||
|
|
ba26887ea5 | ||
|
|
694c45fd96 | ||
|
|
5e165810ec | ||
|
|
99fd93f9a7 | ||
|
|
c2be95777f | ||
|
|
acce25737f | ||
|
|
134f86a580 | ||
|
|
2a81a54609 | ||
|
|
2fcddb5ad5 | ||
|
|
ac0291f09c | ||
|
|
7d1635aff8 | ||
|
|
6c56c95e3f | ||
|
|
d37d9ee859 | ||
|
|
e179e053ce | ||
|
|
546a72ff53 | ||
|
|
c9122eecda | ||
|
|
46d7176976 | ||
|
|
2df1acf532 | ||
|
|
c5bd3a2450 | ||
|
|
260d2904f3 | ||
|
|
8ae57e7ef0 | ||
|
|
b23276f7f3 | ||
|
|
fdc8fcc02b | ||
|
|
ef16fcd505 | ||
|
|
c5898598ae | ||
|
|
461544ed6a | ||
|
|
661b2f1cc0 | ||
|
|
eea660374f | ||
|
|
878a88632b | ||
|
|
22c8f14659 | ||
|
|
d2c4133e56 | ||
|
|
44db557a0c | ||
|
|
31246efc64 | ||
|
|
a7486d723f | ||
|
|
e33b49e2ff | ||
|
|
e51ed89641 | ||
|
|
b459c35219 | ||
|
|
d8512c73df | ||
|
|
fdcaaee955 | ||
|
|
e2858dc90f | ||
|
|
3dbf454d07 | ||
|
|
fc98b951ff | ||
|
|
0b0a9bca4d | ||
|
|
be1c81a778 | ||
|
|
b61723923e |
+5
-1
@@ -65,4 +65,8 @@ data/state.json
|
||||
data/log
|
||||
data-backup/
|
||||
data/coordinator_backup.json
|
||||
data/.storage
|
||||
data/.storage
|
||||
data/database.db.backup
|
||||
|
||||
# MacOS indexing file
|
||||
.DS_Store
|
||||
@@ -16,7 +16,7 @@ steps:
|
||||
inputs:
|
||||
versionSpec: '10.x'
|
||||
displayName: 'Install Node.js'
|
||||
- bash: npm install
|
||||
- bash: npm ci
|
||||
displayName: 'Install dependencies'
|
||||
|
||||
# Verify
|
||||
@@ -30,7 +30,7 @@ steps:
|
||||
displayName: Docker install
|
||||
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
|
||||
inputs:
|
||||
dockerVersion: 19.03.2
|
||||
dockerVersion: 19.03.5
|
||||
releaseType: stable
|
||||
- task: Docker@2
|
||||
displayName: Docker registry login
|
||||
@@ -41,7 +41,7 @@ steps:
|
||||
- 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
|
||||
docker run --rm --privileged hypriot/qemu-register:v2.7.0
|
||||
buildx create --use
|
||||
buildx ls
|
||||
displayName: 'Docker setup'
|
||||
|
||||
+23
-14
@@ -1,21 +1,30 @@
|
||||
FROM node:10-alpine
|
||||
FROM node:12-alpine as base
|
||||
|
||||
# Copy files
|
||||
ADD . /app
|
||||
RUN cp /app/data/configuration.yaml /app
|
||||
RUN cp /app/docker/run.sh /app
|
||||
RUN chmod +x /app/run.sh
|
||||
WORKDIR /app
|
||||
RUN apk add --no-cache tzdata eudev
|
||||
|
||||
COPY package.json .
|
||||
|
||||
# Dependencies
|
||||
FROM base as dependencies
|
||||
|
||||
COPY npm-shrinkwrap.json .
|
||||
|
||||
RUN apk add --no-cache --virtual .buildtools make gcc g++ python linux-headers git && \
|
||||
npm ci --production && \
|
||||
apk del .buildtools
|
||||
|
||||
# Release
|
||||
FROM base as release
|
||||
|
||||
COPY --from=dependencies /app/node_modules ./node_modules
|
||||
COPY lib ./lib
|
||||
COPY index.js docker/run.sh data/configuration.yaml ./
|
||||
|
||||
RUN chmod +x /app/run.sh
|
||||
RUN mkdir /app/data
|
||||
|
||||
# Write .hash.json
|
||||
ARG COMMIT
|
||||
RUN echo "{\"hash\": \"$COMMIT\"}" > .hash.json
|
||||
|
||||
# Install dependencies
|
||||
RUN apk add --update --no-cache tzdata udev eudev && \
|
||||
apk add --update --no-cache --virtual .buildtools make gcc g++ python linux-headers udev git && \
|
||||
npm install --unsafe-perm --production && \
|
||||
apk del .buildtools
|
||||
|
||||
# Entrypoint
|
||||
ENTRYPOINT ["./run.sh"]
|
||||
|
||||
+31
-22
@@ -1,5 +1,6 @@
|
||||
const MQTT = require('./mqtt');
|
||||
const Zigbee = require('./zigbee');
|
||||
const EventBus = require('./eventBus');
|
||||
const State = require('./state');
|
||||
const logger = require('./util/logger');
|
||||
const settings = require('./util/settings');
|
||||
@@ -20,11 +21,13 @@ const ExtensionDeviceAvailability = require('./extension/deviceAvailability');
|
||||
const ExtensionDeviceBind = require('./extension/deviceBind');
|
||||
const ExtensionDeviceReport = require('./extension/deviceReport');
|
||||
const ExtensionDeviceEvent = require('./extension/deviceEvent');
|
||||
const ExtensionOTAUpdate = require('./extension/otaUpdate');
|
||||
|
||||
class Controller {
|
||||
constructor() {
|
||||
this.zigbee = new Zigbee();
|
||||
this.mqtt = new MQTT();
|
||||
this.eventBus = new EventBus();
|
||||
this.state = new State();
|
||||
|
||||
this.publishEntityState = this.publishEntityState.bind(this);
|
||||
@@ -32,39 +35,42 @@ class Controller {
|
||||
|
||||
// Initialize extensions.
|
||||
this.extensions = [
|
||||
new ExtensionEntityPublish(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionDeviceReceive(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionDeviceGroupMembership(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionDeviceConfigure(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionNetworkMap(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionBridgeConfig(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionGroups(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionDeviceBind(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionDeviceEvent(this.zigbee, this.mqtt, this.state, this.publishEntityState),
|
||||
new ExtensionEntityPublish(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
|
||||
new ExtensionDeviceReceive(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
|
||||
new ExtensionDeviceGroupMembership(
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus,
|
||||
),
|
||||
new ExtensionDeviceConfigure(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
|
||||
new ExtensionNetworkMap(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
|
||||
new ExtensionBridgeConfig(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
|
||||
new ExtensionGroups(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
|
||||
new ExtensionDeviceBind(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
|
||||
new ExtensionDeviceEvent(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
|
||||
new ExtensionOTAUpdate(this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus),
|
||||
];
|
||||
|
||||
if (settings.get().advanced.report) {
|
||||
this.extensions.push(new ExtensionDeviceReport(
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState,
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus,
|
||||
));
|
||||
}
|
||||
|
||||
if (settings.get().homeassistant) {
|
||||
this.extensions.push(new ExtensionHomeAssistant(
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState,
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus,
|
||||
));
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (settings.get().advanced.soft_reset_timeout !== 0) {
|
||||
this.extensions.push(new ExtensionSoftReset(
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState,
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus,
|
||||
));
|
||||
}
|
||||
|
||||
if (settings.get().advanced.availability_timeout) {
|
||||
this.extensions.push(new ExtensionDeviceAvailability(
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState,
|
||||
this.zigbee, this.mqtt, this.state, this.publishEntityState, this.eventBus,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -82,8 +88,6 @@ class Controller {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
logger.info(`Logging to directory: '${logger.directory}'`);
|
||||
logger.cleanup();
|
||||
this.state.start();
|
||||
|
||||
const info = await utils.getZigbee2mqttVersion();
|
||||
@@ -91,9 +95,10 @@ class Controller {
|
||||
|
||||
// Start zigbee
|
||||
try {
|
||||
await this.zigbee.start();
|
||||
this.callExtensionMethod('onZigbeeStarted', []);
|
||||
this.zigbee.on('event', this.onZigbeeEvent.bind(this));
|
||||
this.zigbee.on('adapterDisconnected', this.onZigbeeAdapterDisconnected);
|
||||
await this.zigbee.start();
|
||||
} catch (error) {
|
||||
logger.error('Failed to start zigbee');
|
||||
logger.error('Exiting...');
|
||||
@@ -123,10 +128,7 @@ class Controller {
|
||||
logger.warn('Set `permit_join` to `false` once you joined all devices.');
|
||||
}
|
||||
|
||||
this.zigbee.permitJoin(settings.get().permit_join);
|
||||
|
||||
// Call extensions
|
||||
this.callExtensionMethod('onZigbeeStarted', []);
|
||||
await this.zigbee.permitJoin(settings.get().permit_join);
|
||||
|
||||
// MQTT
|
||||
this.mqtt.on('message', this.onMQTTMessage.bind(this));
|
||||
@@ -274,12 +276,19 @@ class Controller {
|
||||
'applicationVersion', 'stackVersion', 'zclVersion', 'hardwareVersion', 'dateCode', 'softwareBuildID',
|
||||
];
|
||||
|
||||
messagePayload.device = {friendlyName: entity.name};
|
||||
messagePayload.device = {
|
||||
friendlyName: entity.name,
|
||||
model: entity.mapped ? entity.mapped.model : 'unknown',
|
||||
};
|
||||
|
||||
attributes.forEach((a) => messagePayload.device[a] = device[a]);
|
||||
}
|
||||
|
||||
if (Object.entries(messagePayload).length) {
|
||||
if (settings.get().experimental.output === 'json') {
|
||||
if (settings.get().experimental.output === 'attribute_and_json') {
|
||||
await this.mqtt.publish(entity.name, JSON.stringify(messagePayload), options);
|
||||
await this.iteratePayloadAttributeOutput(`${entity.name}/`, messagePayload, options);
|
||||
} else if (settings.get().experimental.output === 'json') {
|
||||
await this.mqtt.publish(entity.name, JSON.stringify(messagePayload), options);
|
||||
} else {
|
||||
/* istanbul ignore else */
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
const events = require('events');
|
||||
const assert = require('assert');
|
||||
|
||||
const allowedEvents = [
|
||||
'deviceRemoved', // Device has been removed
|
||||
];
|
||||
|
||||
class EventBus extends events.EventEmitter {
|
||||
emit(event, data) {
|
||||
assert(allowedEvents.includes(event), `Event '${event}' not supported`);
|
||||
super.emit(event, data);
|
||||
}
|
||||
|
||||
on(event, callback) {
|
||||
assert(allowedEvents.includes(event), `Event '${event}' not supported`);
|
||||
super.on(event, callback);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EventBus;
|
||||
@@ -6,12 +6,14 @@ class BaseExtension {
|
||||
* @param {MQTT} mqtt MQTT controller
|
||||
* @param {State} state State controller
|
||||
* @param {Function} publishEntityState Method to publish device state to MQTT.
|
||||
* @param {EventBus} eventBus The event bus
|
||||
*/
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
this.zigbee = zigbee;
|
||||
this.mqtt = mqtt;
|
||||
this.state = state;
|
||||
this.publishEntityState = publishEntityState;
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,12 +5,13 @@ const utils = require('../util/utils');
|
||||
const assert = require('assert');
|
||||
const BaseExtension = require('./baseExtension');
|
||||
|
||||
const configRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/((?:\\w+/get)|(?:\\w+))`);
|
||||
const configRegex =
|
||||
new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/((?:\\w+/get)|(?:\\w+/factory_reset)|(?:\\w+))`);
|
||||
const allowedLogLevels = ['error', 'warn', 'info', 'debug'];
|
||||
|
||||
class BridgeConfig extends BaseExtension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
super(zigbee, mqtt, state, publishEntityState);
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
|
||||
// Bind functions
|
||||
this.permitJoin = this.permitJoin.bind(this);
|
||||
@@ -21,13 +22,17 @@ class BridgeConfig extends BaseExtension {
|
||||
this.devices = this.devices.bind(this);
|
||||
this.groups = this.groups.bind(this);
|
||||
this.rename = this.rename.bind(this);
|
||||
this.renameLast = this.renameLast.bind(this);
|
||||
this.remove = this.remove.bind(this);
|
||||
this.forceRemove = this.forceRemove.bind(this);
|
||||
this.ban = this.ban.bind(this);
|
||||
this.deviceOptions = this.deviceOptions.bind(this);
|
||||
this.addGroup = this.addGroup.bind(this);
|
||||
this.removeGroup = this.removeGroup.bind(this);
|
||||
this.whitelist= this.whitelist.bind(this);
|
||||
this.whitelist = this.whitelist.bind(this);
|
||||
this.touchlinkFactoryReset = this.touchlinkFactoryReset.bind(this);
|
||||
|
||||
this.lastJoinedDeviceName = null;
|
||||
|
||||
// Set supported options
|
||||
this.supportedOptions = {
|
||||
@@ -40,6 +45,7 @@ class BridgeConfig extends BaseExtension {
|
||||
'groups': this.groups,
|
||||
'devices/get': this.devices,
|
||||
'rename': this.rename,
|
||||
'rename_last': this.renameLast,
|
||||
'remove': this.remove,
|
||||
'force_remove': this.forceRemove,
|
||||
'ban': this.ban,
|
||||
@@ -47,6 +53,7 @@ class BridgeConfig extends BaseExtension {
|
||||
'add_group': this.addGroup,
|
||||
'remove_group': this.removeGroup,
|
||||
'whitelist': this.whitelist,
|
||||
'touchlink/factory_reset': this.touchlinkFactoryReset,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -194,12 +201,26 @@ class BridgeConfig extends BaseExtension {
|
||||
return;
|
||||
}
|
||||
|
||||
this._renameInternal(json.old, json.new);
|
||||
}
|
||||
|
||||
renameLast(topic, message) {
|
||||
if (!this.lastJoinedDeviceName) {
|
||||
logger.error(`Cannot rename last joined device, no device has joined during this session`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._renameInternal(this.lastJoinedDeviceName, message);
|
||||
}
|
||||
|
||||
_renameInternal(from, to) {
|
||||
try {
|
||||
settings.changeFriendlyName(json.old, json.new);
|
||||
logger.info(`Successfully renamed - ${json.old} to ${json.new} `);
|
||||
this.mqtt.log('device_renamed', {from: json.old, to: json.new});
|
||||
const isGroup = settings.getGroup(from) !== null;
|
||||
settings.changeFriendlyName(from, to);
|
||||
logger.info(`Successfully renamed - ${from} to ${to} `);
|
||||
this.mqtt.log(`${isGroup ? 'group' : 'device'}_renamed`, {from, to});
|
||||
} catch (error) {
|
||||
logger.error(`Failed to rename - ${json.old} to ${json.new}`);
|
||||
logger.error(`Failed to rename - ${from} to ${to}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +254,10 @@ class BridgeConfig extends BaseExtension {
|
||||
|
||||
removeGroup(topic, message) {
|
||||
const name = message;
|
||||
settings.removeGroup(name);
|
||||
const entity = this.zigbee.resolveEntity(message);
|
||||
assert(entity && entity.type === 'group', `Group '${message}' does not exist`);
|
||||
settings.removeGroup(message);
|
||||
entity.group.removeFromDatabase();
|
||||
logger.info(`Removed group '${name}'`);
|
||||
}
|
||||
|
||||
@@ -259,10 +283,15 @@ class BridgeConfig extends BaseExtension {
|
||||
|
||||
if (!entity) {
|
||||
logger.error(`Cannot ${lookup[action][2]}, device '${message}' does not exist`);
|
||||
|
||||
this.mqtt.log(`device_${lookup[action][0]}_failed`, message);
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
// Fire event
|
||||
this.eventBus.emit('deviceRemoved', {device: entity.device});
|
||||
|
||||
// Remove from configuration.yaml
|
||||
settings.removeDevice(entity.settings.ID);
|
||||
|
||||
@@ -284,6 +313,10 @@ class BridgeConfig extends BaseExtension {
|
||||
cleanup();
|
||||
} catch (error) {
|
||||
logger.error(`Failed to ${lookup[action][2]} ${entity.settings.friendlyName} (${error})`);
|
||||
// eslint-disable-next-line
|
||||
logger.error(`See https://www.zigbee2mqtt.io/information/mqtt_topics_and_message_structure.html#zigbee2mqttbridgeconfigremove for more info`);
|
||||
|
||||
this.mqtt.log(`device_${lookup[action][0]}_failed`, message);
|
||||
}
|
||||
|
||||
if (action === 'ban') {
|
||||
@@ -293,6 +326,7 @@ class BridgeConfig extends BaseExtension {
|
||||
|
||||
async onMQTTConnected() {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/config/+`);
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/config/+/+`);
|
||||
await this.publish();
|
||||
}
|
||||
|
||||
@@ -326,6 +360,23 @@ class BridgeConfig extends BaseExtension {
|
||||
|
||||
await this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0});
|
||||
}
|
||||
|
||||
onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
|
||||
if (type === 'deviceJoined') {
|
||||
this.lastJoinedDeviceName = settingsDevice.friendlyName;
|
||||
}
|
||||
}
|
||||
|
||||
async touchlinkFactoryReset() {
|
||||
logger.info('Starting touchlink factory reset...');
|
||||
const result = await this.zigbee.touchlinkFactoryReset();
|
||||
|
||||
if (result) {
|
||||
logger.info('Successfully factory reset device through Touchlink');
|
||||
} else {
|
||||
logger.warn('Failed to factory reset device through Touchlink');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BridgeConfig;
|
||||
|
||||
@@ -6,18 +6,24 @@ const BaseExtension = require('./baseExtension');
|
||||
|
||||
// Some EndDevices should be pinged
|
||||
// e.g. E11-G13 https://github.com/Koenkk/zigbee2mqtt/issues/775#issuecomment-453683846
|
||||
const forcedPingable = [
|
||||
zigbeeHerdsmanConverters.devices.find((d) => d.model === 'E11-G13'),
|
||||
const pingableModels = [
|
||||
'E11-G13',
|
||||
'53170161',
|
||||
];
|
||||
|
||||
const forcedPingable =
|
||||
zigbeeHerdsmanConverters.devices.filter((d) => pingableModels.includes(d.model));
|
||||
|
||||
const toZigbeeCandidates = ['state', 'brightness', 'color', 'color_temp'];
|
||||
|
||||
const Hours25 = 1000 * 60 * 60 * 25;
|
||||
|
||||
/**
|
||||
* This extensions pings devices to check if they are online.
|
||||
*/
|
||||
class DeviceAvailability extends BaseExtension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
super(zigbee, mqtt, state, publishEntityState);
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
|
||||
this.availability_timeout = settings.get().advanced.availability_timeout;
|
||||
this.timers = {};
|
||||
@@ -27,39 +33,71 @@ class DeviceAvailability extends BaseExtension {
|
||||
this.blacklist = settings.get().advanced.availability_blacklist.map((e) => {
|
||||
return settings.getEntity(e).ID;
|
||||
});
|
||||
|
||||
// Initialize whitelist
|
||||
this.whitelist = settings.get().advanced.availability_whitelist.map((e) => {
|
||||
return settings.getEntity(e).ID;
|
||||
});
|
||||
}
|
||||
|
||||
isPingable(device) {
|
||||
if (this.blacklist.includes(device.ieeeAddr)) {
|
||||
isAllowed(device) {
|
||||
const ieeeAddr = device.ieeeAddr;
|
||||
|
||||
const deviceSettings = settings.getDevice(ieeeAddr);
|
||||
const name = deviceSettings ? deviceSettings.friendly_name : null;
|
||||
|
||||
// Whitelist is not empty and device is in it, enable availability
|
||||
if (this.whitelist.length > 0) {
|
||||
return this.whitelist.includes(ieeeAddr) || (name && this.whitelist.includes(name));
|
||||
}
|
||||
|
||||
// Device is on blacklist, disable availability
|
||||
if (this.blacklist.includes(ieeeAddr) || (name && this.blacklist.includes(name))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
isPingable(device) {
|
||||
// Device is on forcedPingable-list, enable availability
|
||||
if (forcedPingable.find((d) => d.zigbeeModel.includes(device.modelID))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Device is a mains powered router
|
||||
const result = utils.isRouter(device) && !utils.isBatteryPowered(device);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getAllPingableDevices() {
|
||||
return this.zigbee.getClients().filter((d) => this.isPingable(d));
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
// As some devices are not checked for availability (e.g. battery powered devices)
|
||||
// we mark all device as online by default.
|
||||
this.zigbee.getClients().forEach((device) => this.publishAvailability(device, true));
|
||||
for (const device of this.zigbee.getClients()) {
|
||||
// Mark all devices as online on start
|
||||
this.publishAvailability(device, true);
|
||||
|
||||
// Start timers for all devices
|
||||
this.getAllPingableDevices().forEach((device) => this.setTimer(device));
|
||||
if (this.isAllowed(device)) {
|
||||
if (this.isPingable(device)) {
|
||||
this.setTimerPingable(device);
|
||||
} else {
|
||||
this.timers[device.ieeeAddr] = setInterval(() => {
|
||||
this.handleIntervalNotPingable(device);
|
||||
}, utils.secondsToMilliseconds(300));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleInterval(device) {
|
||||
async handleIntervalPingable(device) {
|
||||
// When a device is already unavailable, log the ping failed on 'debug' instead of 'error'.
|
||||
const entity = this.zigbee.resolveEntity(device.ieeeAddr);
|
||||
if (!entity) {
|
||||
logger.debug(`Stop pinging '${device.ieeeAddr}', device is not known anymore`);
|
||||
return;
|
||||
}
|
||||
|
||||
const ieeeAddr = device.ieeeAddr;
|
||||
const level = this.state.hasOwnProperty(ieeeAddr) && !this.state[ieeeAddr] ? 'debug' : 'error';
|
||||
const entity = this.zigbee.resolveEntity(device.ieeeAddr);
|
||||
try {
|
||||
await device.ping();
|
||||
this.publishAvailability(device, true);
|
||||
@@ -68,17 +106,31 @@ class DeviceAvailability extends BaseExtension {
|
||||
this.publishAvailability(device, false);
|
||||
logger[level](`Failed to ping '${entity.name}'`);
|
||||
} finally {
|
||||
this.setTimer(device);
|
||||
this.setTimerPingable(device);
|
||||
}
|
||||
}
|
||||
|
||||
setTimer(device) {
|
||||
async handleIntervalNotPingable(device) {
|
||||
const ago = Date.now() - device.lastSeen;
|
||||
const entity = this.zigbee.resolveEntity(device.ieeeAddr);
|
||||
if (!entity || !device.lastSeen) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`Non-pingable device '${entity.name}' was last seen '${ago / 1000}' seconds ago.`);
|
||||
|
||||
if (ago > Hours25) {
|
||||
this.publishAvailability(device, false);
|
||||
}
|
||||
}
|
||||
|
||||
setTimerPingable(device) {
|
||||
if (this.timers[device.ieeeAddr]) {
|
||||
clearTimeout(this.timers[device.ieeeAddr]);
|
||||
}
|
||||
|
||||
this.timers[device.ieeeAddr] = setTimeout(async () => {
|
||||
await this.handleInterval(device);
|
||||
await this.handleIntervalPingable(device);
|
||||
}, utils.secondsToMilliseconds(this.availability_timeout));
|
||||
}
|
||||
|
||||
@@ -96,12 +148,17 @@ class DeviceAvailability extends BaseExtension {
|
||||
|
||||
if (mappedDevice) {
|
||||
const used = [];
|
||||
for (const key of toZigbeeCandidates) {
|
||||
const converter = mappedDevice.toZigbee.find((tz) => tz.key.includes(key));
|
||||
if (converter && !used.includes(converter)) {
|
||||
converter.convertGet(device.endpoints[0], key, {});
|
||||
used.push(converter);
|
||||
try {
|
||||
for (const key of toZigbeeCandidates) {
|
||||
const converter = mappedDevice.toZigbee.find((tz) => tz.key.includes(key));
|
||||
if (converter && !used.includes(converter)) {
|
||||
await converter.convertGet(device.endpoints[0], key, {});
|
||||
used.push(converter);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const entity = this.zigbee.resolveEntity(device.ieeeAddr);
|
||||
logger.error(`Failed to read state of '${entity.name}' after reconnect`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,12 +170,14 @@ class DeviceAvailability extends BaseExtension {
|
||||
this.onReconnect(device);
|
||||
}
|
||||
|
||||
this.state[ieeeAddr] = available;
|
||||
const deviceSettings = settings.getDevice(ieeeAddr);
|
||||
const name = deviceSettings ? deviceSettings.friendly_name : ieeeAddr;
|
||||
const topic = `${name}/availability`;
|
||||
const payload = available ? 'online' : 'offline';
|
||||
this.mqtt.publish(topic, payload, {retain: true, qos: 0});
|
||||
if (this.state[ieeeAddr] !== available) {
|
||||
this.state[ieeeAddr] = available;
|
||||
this.mqtt.publish(topic, payload, {retain: true, qos: 0});
|
||||
}
|
||||
}
|
||||
|
||||
onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
|
||||
@@ -127,29 +186,22 @@ class DeviceAvailability extends BaseExtension {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isPingable(device)) {
|
||||
// When a zigbee message from a device is received we know the device is still alive.
|
||||
// => reset the timer.
|
||||
this.setTimer(device);
|
||||
if (this.isAllowed(device)) {
|
||||
this.publishAvailability(data.device, true);
|
||||
|
||||
const online = this.state.hasOwnProperty(device.ieeeAddr) && this.state[device.ieeeAddr];
|
||||
const offline = this.state.hasOwnProperty(device.ieeeAddr) && !this.state[device.ieeeAddr];
|
||||
if (this.isPingable(device)) {
|
||||
// When a zigbee message from a device is received we know the device is still alive.
|
||||
// => reset the timer.
|
||||
this.setTimerPingable(device);
|
||||
|
||||
if (!online && !offline) {
|
||||
// A new device has been connected
|
||||
this.publishAvailability(device, true);
|
||||
} else if (offline) {
|
||||
// When a message is received and the device is marked as offline, mark it online.
|
||||
this.publishAvailability(device, true);
|
||||
} else {
|
||||
/* istanbul ignore else */
|
||||
const online = this.state.hasOwnProperty(device.ieeeAddr) && this.state[device.ieeeAddr];
|
||||
if (online && type === 'deviceAnnounce' && !utils.isIkeaTradfriDevice(device)) {
|
||||
/**
|
||||
* In case the device is powered off AND on within the availability timeout,
|
||||
* zigbee2qmtt does not detect the device as offline (device is still marked online).
|
||||
* When a device is turned on again the state could be out of sync.
|
||||
* https://github.com/Koenkk/zigbee2mqtt/issues/1383#issuecomment-489412168
|
||||
* endDeviceAnnce is typically send when a device comes online.
|
||||
* deviceAnnounce is typically send when a device comes online.
|
||||
*
|
||||
* This isn't needed for TRADFRI devices as they already send the state themself.
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,10 @@ const assert = require('assert');
|
||||
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/(bind|unbind)/.+$`);
|
||||
const BaseExtension = require('./baseExtension');
|
||||
|
||||
const clusters = ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl'];
|
||||
const clusters = ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl', 'closuresWindowCovering'];
|
||||
|
||||
// See zigbee-herdsman-converters devices.js
|
||||
const defaultBindGroup = {type: 'group_number', ID: 901};
|
||||
|
||||
class DeviceBind extends BaseExtension {
|
||||
onMQTTConnected() {
|
||||
@@ -28,22 +31,26 @@ class DeviceBind extends BaseExtension {
|
||||
// Find source; can only be a device and target
|
||||
const source = this.zigbee.resolveEntity(sourceKey);
|
||||
assert(source != null && source.type === 'device', 'Source undefined or not a device');
|
||||
const target = this.zigbee.resolveEntity(targetKey);
|
||||
const target = targetKey === 'default_bind_group' ? defaultBindGroup : this.zigbee.resolveEntity(targetKey);
|
||||
assert(target != null, 'Target is unknown');
|
||||
|
||||
const sourceName = source.settings.friendlyName;
|
||||
const targetName = target.settings.friendlyName;
|
||||
const targetName = targetKey === 'default_bind_group' ? targetKey : target.settings.friendlyName;
|
||||
|
||||
// Find which clusters are supported by both the source and target.
|
||||
// Groups are assumed to support all clusters.
|
||||
for (const cluster of clusters) {
|
||||
const targetValid = target.type === 'group' ||
|
||||
const targetValid = target.type === 'group' || target.type === 'group_number' ||
|
||||
target.device.type === 'Coordinator' || target.endpoint.supportsInputCluster(cluster);
|
||||
|
||||
if (source.endpoint.supportsOutputCluster(cluster) && targetValid) {
|
||||
logger.debug(`${type}ing cluster '${cluster}' from '${sourceName}' to '${targetName}'`);
|
||||
try {
|
||||
const bindTarget = target.type === 'group' ? target.group : target.endpoint;
|
||||
let bindTarget = null;
|
||||
if (target.type === 'group') bindTarget = target.group;
|
||||
else if (target.type === 'group_number') bindTarget = target.ID;
|
||||
else bindTarget = target.endpoint;
|
||||
|
||||
if (type === 'bind') {
|
||||
await source.endpoint.bind(cluster, bindTarget);
|
||||
} else {
|
||||
@@ -63,6 +70,10 @@ class DeviceBind extends BaseExtension {
|
||||
`Failed to ${type} cluster '${cluster}' from '${sourceName}' to ` +
|
||||
`'${targetName}' (${error})`,
|
||||
);
|
||||
this.mqtt.log(
|
||||
`device_${type}_failed`,
|
||||
{from: sourceName, to: targetName, cluster},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,23 +4,22 @@ const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
const BaseExtension = require('./baseExtension');
|
||||
|
||||
class DeviceConfigure extends BaseExtension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
super(zigbee, mqtt, state, publishEntityState);
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
|
||||
this.configuring = new Set();
|
||||
this.attempts = {};
|
||||
this.topic = `${settings.get().mqtt.base_topic}/bridge/configure`;
|
||||
}
|
||||
|
||||
shouldConfigure(device, mappedDevice) {
|
||||
if (!device) {
|
||||
if (!device || !mappedDevice || !mappedDevice.configure) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (device.meta.hasOwnProperty('configured') && device.meta.configured === mappedDevice.meta.configureKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mappedDevice || !mappedDevice.configure) {
|
||||
if (device.meta &&
|
||||
device.meta.hasOwnProperty('configured') &&
|
||||
device.meta.configured === mappedDevice.meta.configureKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -31,8 +30,32 @@ class DeviceConfigure extends BaseExtension {
|
||||
return true;
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
this.mqtt.subscribe(this.topic);
|
||||
}
|
||||
|
||||
async onMQTTMessage(topic, message) {
|
||||
if (topic !== this.topic) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entity = this.zigbee.resolveEntity(message);
|
||||
if (!entity || entity.type !== 'device') {
|
||||
logger.error(`Device '${message}' does not exist`);
|
||||
return;
|
||||
}
|
||||
|
||||
const mappedDevice = zigbeeHerdsmanConverters.findByZigbeeModel(entity.device.modelID);
|
||||
if (!mappedDevice.configure) {
|
||||
logger.warn(`Skipping configure of '${entity.name}', device does not require this.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.configure(entity.device, mappedDevice, entity.settings, true);
|
||||
}
|
||||
|
||||
async onZigbeeStarted() {
|
||||
this.coordinatorEndpoint = this.zigbee.getDevicesByType('Coordinator')[0].endpoints[0];
|
||||
this.coordinatorEndpoint = this.zigbee.getDevicesByType('Coordinator')[0].getEndpoint(1);
|
||||
|
||||
for (const device of this.zigbee.getClients()) {
|
||||
const mappedDevice = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
|
||||
@@ -45,13 +68,19 @@ class DeviceConfigure extends BaseExtension {
|
||||
|
||||
onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
|
||||
const device = data.device;
|
||||
|
||||
if (type === 'deviceJoined' && device.meta.hasOwnProperty('configured')) {
|
||||
delete device.meta.configured;
|
||||
device.save();
|
||||
}
|
||||
|
||||
if (this.shouldConfigure(device, mappedDevice)) {
|
||||
this.configure(device, mappedDevice, settingsDevice);
|
||||
}
|
||||
}
|
||||
|
||||
async configure(device, mappedDevice, settingsDevice) {
|
||||
if (this.configuring.has(device.ieeeAddr) || this.attempts[device.ieeeAddr] >= 3) {
|
||||
async configure(device, mappedDevice, settingsDevice, force=false) {
|
||||
if (this.configuring.has(device.ieeeAddr) || (this.attempts[device.ieeeAddr] >= 3 && !force)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -64,7 +93,7 @@ class DeviceConfigure extends BaseExtension {
|
||||
logger.info(`Configuring '${settingsDevice.friendlyName}'`);
|
||||
try {
|
||||
await mappedDevice.configure(device, this.coordinatorEndpoint);
|
||||
logger.info(`Succesfully configured '${settingsDevice.friendlyName}'`);
|
||||
logger.info(`Successfully configured '${settingsDevice.friendlyName}'`);
|
||||
// eslint-disable-next-line
|
||||
device.meta.configured = mappedDevice.meta.configureKey;
|
||||
device.save();
|
||||
|
||||
@@ -21,6 +21,8 @@ class DeviceEvent extends BaseExtension {
|
||||
}
|
||||
|
||||
callOnEvent(device, type, data, mappedDevice) {
|
||||
zigbeeHerdsmanConverters.onEvent(type, data, device);
|
||||
|
||||
if (!mappedDevice) {
|
||||
mappedDevice = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/device/
|
||||
|
||||
class DeviceGroupMembership extends BaseExtension {
|
||||
onMQTTConnected() {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/device/+/get_group_membership`);
|
||||
for (let step = 1; step < 20; step++) {
|
||||
const topic = `${settings.get().mqtt.base_topic}/bridge/device/${'+/'.repeat(step)}get_group_membership`;
|
||||
this.mqtt.subscribe(topic);
|
||||
}
|
||||
}
|
||||
|
||||
async onMQTTMessage(topic, message) {
|
||||
|
||||
Regular → Executable
+35
-8
@@ -5,8 +5,8 @@ const debounce = require('debounce');
|
||||
const BaseExtension = require('./baseExtension');
|
||||
|
||||
class DeviceReceive extends BaseExtension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
super(zigbee, mqtt, state, publishEntityState);
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
this.coordinator = null;
|
||||
this.elapsed = {};
|
||||
this.debouncers = {};
|
||||
@@ -16,7 +16,7 @@ class DeviceReceive extends BaseExtension {
|
||||
this.coordinator = this.zigbee.getDevicesByType('Coordinator')[0];
|
||||
}
|
||||
|
||||
publishDebounce(ieeeAddr, payload, time) {
|
||||
publishDebounce(ieeeAddr, payload, time, debounceIgnore) {
|
||||
if (!this.debouncers[ieeeAddr]) {
|
||||
this.debouncers[ieeeAddr] = {
|
||||
payload: {},
|
||||
@@ -27,10 +27,33 @@ class DeviceReceive extends BaseExtension {
|
||||
};
|
||||
}
|
||||
|
||||
if (this.isPayloadConflicted(payload, this.debouncers[ieeeAddr].payload, debounceIgnore)) {
|
||||
// publish previous payload immediately
|
||||
this.debouncers[ieeeAddr].publish.flush();
|
||||
}
|
||||
|
||||
// extend debounced payload with current
|
||||
this.debouncers[ieeeAddr].payload = {...this.debouncers[ieeeAddr].payload, ...payload};
|
||||
this.debouncers[ieeeAddr].publish();
|
||||
}
|
||||
|
||||
// if debounce_ignore are specified (Array of strings)
|
||||
// then all newPayload values with key present in debounce_ignore
|
||||
// should equal or be undefined in oldPayload
|
||||
// otherwise payload is conflicted
|
||||
isPayloadConflicted(newPayload, oldPayload, debounceIgnore) {
|
||||
let result = false;
|
||||
Object.keys(oldPayload)
|
||||
.filter((key) => (debounceIgnore || []).includes(key))
|
||||
.forEach((key) => {
|
||||
if (typeof newPayload[key] !== 'undefined' && newPayload[key] !== oldPayload[key]) {
|
||||
result = true;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
canHandleEvent(type, data, mappedDevice) {
|
||||
if (type !== 'message') {
|
||||
return false;
|
||||
@@ -86,9 +109,7 @@ class DeviceReceive extends BaseExtension {
|
||||
|
||||
// Check if there is an available converter
|
||||
if (!converters.length) {
|
||||
// Don't log readRsp and raw as warn messages, they are not interesting most of the time.
|
||||
const level = ['readResponse', 'raw'].includes(data.type) ? 'debug' : 'warn';
|
||||
logger[level](
|
||||
logger.debug(
|
||||
`No converter available for '${mappedDevice.model}' with cluster '${data.cluster}' ` +
|
||||
`and type '${data.type}' and data '${JSON.stringify(data.data)}'`,
|
||||
);
|
||||
@@ -117,7 +138,12 @@ class DeviceReceive extends BaseExtension {
|
||||
|
||||
// Check if we have to debounce
|
||||
if (settingsDevice && settingsDevice.hasOwnProperty('debounce')) {
|
||||
this.publishDebounce(data.device.ieeeAddr, payload, settingsDevice.debounce);
|
||||
this.publishDebounce(
|
||||
data.device.ieeeAddr,
|
||||
payload,
|
||||
settingsDevice.debounce,
|
||||
settingsDevice.debounce_ignore,
|
||||
);
|
||||
} else {
|
||||
this.publishEntityState(data.device.ieeeAddr, payload);
|
||||
|
||||
@@ -138,10 +164,11 @@ class DeviceReceive extends BaseExtension {
|
||||
}
|
||||
};
|
||||
|
||||
const meta = {device: data.device};
|
||||
let payload = {};
|
||||
converters.forEach((converter) => {
|
||||
const options = {...settings.get().device_options, ...settings.getDevice(data.device.ieeeAddr)};
|
||||
const converted = converter.convert(mappedDevice, data, publish, options);
|
||||
const converted = converter.convert(mappedDevice, data, publish, options, meta);
|
||||
if (converted) {
|
||||
payload = {...payload, ...converted};
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
const logger = require('../util/logger');
|
||||
const CC2530Router = zigbeeHerdsmanConverters.devices.find((d) => d.model === 'CC2530.ROUTER');
|
||||
const BASICZBR3 = zigbeeHerdsmanConverters.devices.find((d) => d.model === 'BASICZBR3');
|
||||
const ZNLDP12LM = zigbeeHerdsmanConverters.devices.find((d) => d.model === 'ZNLDP12LM');
|
||||
const utils = require('../util/utils');
|
||||
const BaseExtension = require('./baseExtension');
|
||||
const debounce = require('debounce');
|
||||
const ZigbeeHerdsman = require('zigbee-herdsman');
|
||||
|
||||
const defaultConfiguration = {
|
||||
minimumReportInterval: 3, maximumReportInterval: 300, reportableChange: 0,
|
||||
minimumReportInterval: 3, maximumReportInterval: 300, reportableChange: 1,
|
||||
};
|
||||
|
||||
const devicesNotSupportingReporting = [CC2530Router, BASICZBR3];
|
||||
|
||||
const reportKey = 1;
|
||||
|
||||
const clusters = {
|
||||
@@ -54,29 +58,55 @@ const pollOnMessage = [
|
||||
// When the bound devices/members of group have the following manufacturerID
|
||||
manufacturerID: ZigbeeHerdsman.Zcl.ManufacturerCode.Philips,
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
cluster: {
|
||||
genOnOff: [
|
||||
{type: 'commandOn', data: {}},
|
||||
{type: 'commandOff', data: {}},
|
||||
{type: 'commandOffWithEffect', data: {}},
|
||||
],
|
||||
manuSpecificPhilips: [
|
||||
{type: 'commandHueNotification', data: {button: 1}},
|
||||
{type: 'commandHueNotification', data: {button: 4}},
|
||||
],
|
||||
},
|
||||
read: {cluster: 'genOnOff', attributes: ['onOff']},
|
||||
manufacturerID: ZigbeeHerdsman.Zcl.ManufacturerCode.Philips,
|
||||
},
|
||||
];
|
||||
|
||||
class DeviceReport extends BaseExtension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
super(zigbee, mqtt, state, publishEntityState);
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
this.configuring = new Set();
|
||||
this.failed = new Set();
|
||||
this.pollDebouncers = {};
|
||||
}
|
||||
|
||||
async setupReporting(device) {
|
||||
shouldIgnoreClusterForDevice(cluster, mappedDevice) {
|
||||
if (mappedDevice === ZNLDP12LM && cluster === 'closuresWindowCovering') {
|
||||
// Device announces it but doesn't support it
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/2611
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async setupReporting(device, mappedDevice) {
|
||||
if (this.configuring.has(device.ieeeAddr) || this.failed.has(device.ieeeAddr)) return;
|
||||
this.configuring.add(device.ieeeAddr);
|
||||
|
||||
try {
|
||||
for (const endpoint of device.endpoints) {
|
||||
for (const ep of device.endpoints) {
|
||||
for (const [cluster, configuration] of Object.entries(clusters)) {
|
||||
if (endpoint.supportsInputCluster(cluster)) {
|
||||
logger.debug(`Setup reporting for '${device.ieeeAddr}' - ${endpoint.ID} - ${cluster}`);
|
||||
await endpoint.bind(cluster, this.coordinatorEndpoint);
|
||||
await endpoint.configureReporting(cluster, configuration);
|
||||
if (ep.supportsInputCluster(cluster) && !this.shouldIgnoreClusterForDevice(cluster, mappedDevice)) {
|
||||
logger.debug(`Setup reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`);
|
||||
await ep.bind(cluster, this.coordinatorEndpoint);
|
||||
await ep.configureReporting(cluster, configuration);
|
||||
logger.info(
|
||||
`Succesfully setup reporting for '${device.ieeeAddr}' - ${endpoint.ID} - ${cluster}`,
|
||||
`Successfully setup reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -97,7 +127,7 @@ class DeviceReport extends BaseExtension {
|
||||
}
|
||||
|
||||
shouldSetupReporting(mappedDevice, device, messageType) {
|
||||
if (!device) return false;
|
||||
if (!device || !mappedDevice) return false;
|
||||
|
||||
// Handle messages of type endDeviceAnnce and devIncoming.
|
||||
// This message is typically send when a device comes online after being powered off
|
||||
@@ -109,24 +139,24 @@ class DeviceReport extends BaseExtension {
|
||||
if (device.meta.hasOwnProperty('reporting') && device.meta.reporting === reportKey) return false;
|
||||
if (!utils.isRouter(device) || utils.isBatteryPowered(device)) return false;
|
||||
// Gledopto devices don't support reporting.
|
||||
if (mappedDevice === CC2530Router || mappedDevice.vendor === 'Gledopto') return false;
|
||||
if (devicesNotSupportingReporting.includes(mappedDevice) || mappedDevice.vendor === 'Gledopto') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
async onZigbeeStarted() {
|
||||
this.coordinatorEndpoint = this.zigbee.getDevicesByType('Coordinator')[0].endpoints[0];
|
||||
this.coordinatorEndpoint = this.zigbee.getDevicesByType('Coordinator')[0].getEndpoint(1);
|
||||
|
||||
for (const device of this.zigbee.getClients()) {
|
||||
const mappedDevice = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
|
||||
if (this.shouldSetupReporting(mappedDevice, device, null)) {
|
||||
this.setupReporting(device);
|
||||
this.setupReporting(device, mappedDevice);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
|
||||
if (this.shouldSetupReporting(mappedDevice, data.device, type)) {
|
||||
this.setupReporting(data.device);
|
||||
this.setupReporting(data.device, mappedDevice);
|
||||
}
|
||||
|
||||
if (type === 'message') {
|
||||
|
||||
@@ -15,6 +15,10 @@ const groupConverters = [
|
||||
zigbeeHerdsmanConverters.toZigbeeConverters.light_color,
|
||||
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,
|
||||
];
|
||||
|
||||
class EntityPublish extends BaseExtension {
|
||||
@@ -37,7 +41,7 @@ class EntityPublish extends BaseExtension {
|
||||
}
|
||||
|
||||
const ID = match[1].replace(`${settings.get().mqtt.base_topic}/`, '');
|
||||
// If we didn't repalce base_topic we received something we don't care about
|
||||
// If we didn't replace base_topic we received something we don't care about
|
||||
if (ID === match[1] || ID.match(/bridge/)) {
|
||||
return null;
|
||||
}
|
||||
@@ -124,7 +128,22 @@ class EntityPublish extends BaseExtension {
|
||||
|
||||
// For each attribute call the corresponding converter
|
||||
const usedConverters = [];
|
||||
for (const [key, value] of entries) {
|
||||
for (let [key, value] of entries) {
|
||||
let postfix = topic.postfix;
|
||||
let actualTarget = target;
|
||||
|
||||
// When the key has a postfix included (e.g. state_right), this will override the target.
|
||||
if (entity.type === 'device' && key.includes('_')) {
|
||||
const underscoreIndex = key.lastIndexOf('_');
|
||||
const possiblePostfix = key.substring(underscoreIndex + 1, key.length);
|
||||
if (utils.getPostfixes().includes(possiblePostfix)) {
|
||||
postfix = possiblePostfix;
|
||||
key = key.substring(0, underscoreIndex);
|
||||
const device = target.getDevice();
|
||||
actualTarget = device.getEndpoint(mapped.endpoint(device)[postfix]);
|
||||
}
|
||||
}
|
||||
|
||||
const converter = converters.find((c) => c.key.includes(key));
|
||||
|
||||
if (usedConverters.includes(converter)) {
|
||||
@@ -139,7 +158,7 @@ class EntityPublish extends BaseExtension {
|
||||
|
||||
// Converter didn't return a result, skip
|
||||
const meta = {
|
||||
endpoint_name: topic.postfix,
|
||||
endpoint_name: postfix,
|
||||
options,
|
||||
message: json,
|
||||
logger,
|
||||
@@ -149,14 +168,14 @@ class EntityPublish extends BaseExtension {
|
||||
};
|
||||
|
||||
try {
|
||||
if (topic.type === 'set' && converter && converter.convertSet) {
|
||||
if (topic.type === 'set' && converter.convertSet) {
|
||||
logger.debug(`Publishing '${topic.type}' '${key}' to '${entity.name}'`);
|
||||
const result = await converter.convertSet(target, key, value, meta);
|
||||
if (result && result.hasOwnProperty('state')) {
|
||||
const result = await converter.convertSet(actualTarget, key, value, meta);
|
||||
if (result && result.state) {
|
||||
const msg = result.state;
|
||||
|
||||
if (topic.postfix) {
|
||||
msg[`state_${topic.postfix}`] = msg.state;
|
||||
if (postfix) {
|
||||
msg[`state_${postfix}`] = msg.state;
|
||||
delete msg.state;
|
||||
}
|
||||
|
||||
@@ -174,11 +193,11 @@ class EntityPublish extends BaseExtension {
|
||||
entity.type === 'device' && result && result.hasOwnProperty('readAfterWriteTime') &&
|
||||
entity.settings.retrieve_state
|
||||
) {
|
||||
setTimeout(() => converter.convertGet(target, key, meta), result.readAfterWriteTime);
|
||||
setTimeout(() => converter.convertGet(actualTarget, key, meta), result.readAfterWriteTime);
|
||||
}
|
||||
} else if (topic.type === 'get' && converter && converter.convertGet) {
|
||||
} else if (topic.type === 'get' && converter.convertGet) {
|
||||
logger.debug(`Publishing get '${topic.type}' '${key}' to '${entity.name}'`);
|
||||
await converter.convertGet(target, key, meta);
|
||||
await converter.convertGet(actualTarget, key, meta);
|
||||
} else {
|
||||
logger.error(`No converter available for '${topic.type}' '${key}' (${json[key]})`);
|
||||
continue;
|
||||
@@ -187,6 +206,7 @@ class EntityPublish extends BaseExtension {
|
||||
const message =
|
||||
`Publish '${topic.type}' '${key}' to '${entity.name}' failed: '${error}'`;
|
||||
logger.error(message);
|
||||
logger.debug(error.stack);
|
||||
this.mqtt.log('zigbee_publish_error', message, {friendly_name: entity.name});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/(
|
||||
const topicRegexRemoveAll = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/remove_all$`);
|
||||
|
||||
class Groups extends BaseExtension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
super(zigbee, mqtt, state, publishEntityState);
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
this.onStateChange = this.onStateChange.bind(this);
|
||||
}
|
||||
|
||||
@@ -142,6 +142,9 @@ class Groups extends BaseExtension {
|
||||
|
||||
if (!group || group.type !== 'group') {
|
||||
logger.error(`Group '${topicMatch[1]}' does not exist`);
|
||||
this.mqtt.log(
|
||||
`device_group_${type}_failed`,
|
||||
{friendly_name: message, group: topicMatch[1], error: 'group doesn\'t exists'});
|
||||
return;
|
||||
}
|
||||
} else if (topic.match(topicRegexRemoveAll)) {
|
||||
@@ -153,6 +156,9 @@ class Groups extends BaseExtension {
|
||||
const entity = this.zigbee.resolveEntity(message);
|
||||
if (!entity || !entity.type === 'device') {
|
||||
logger.error(`Device '${message}' does not exist`);
|
||||
this.mqtt.log(
|
||||
`device_group_${type}_failed`,
|
||||
{friendly_name: message, group: topicMatch[1], error: 'entity doesn\'t exists'});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+363
-50
@@ -26,6 +26,15 @@ const cfg = {
|
||||
device_class: 'presence',
|
||||
},
|
||||
},
|
||||
'binary_sensor_tamper': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'tamper',
|
||||
discovery_payload: {
|
||||
payload_on: false,
|
||||
payload_off: true,
|
||||
value_template: '{{ value_json.tamper }}',
|
||||
},
|
||||
},
|
||||
'binary_sensor_contact': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'contact',
|
||||
@@ -96,6 +105,15 @@ const cfg = {
|
||||
device_class: 'battery',
|
||||
},
|
||||
},
|
||||
'binary_sensor_update_available': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'update_available',
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.update_available}}',
|
||||
},
|
||||
},
|
||||
'binary_sensor_lock': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'lock',
|
||||
@@ -122,11 +140,20 @@ const cfg = {
|
||||
type: 'sensor',
|
||||
object_id: 'illuminance',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: 'lx',
|
||||
unit_of_measurement: '-',
|
||||
device_class: 'illuminance',
|
||||
value_template: '{{ value_json.illuminance }}',
|
||||
},
|
||||
},
|
||||
'sensor_illuminance_lux': {
|
||||
type: 'sensor',
|
||||
object_id: 'illuminance',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: 'lx',
|
||||
device_class: 'illuminance',
|
||||
value_template: '{{ value_json.illuminance_lux }}',
|
||||
},
|
||||
},
|
||||
'sensor_humidity': {
|
||||
type: 'sensor',
|
||||
object_id: 'humidity',
|
||||
@@ -145,6 +172,15 @@ const cfg = {
|
||||
value_template: '{{ value_json.temperature }}',
|
||||
},
|
||||
},
|
||||
'sensor_local_temperature': {
|
||||
type: 'sensor',
|
||||
object_id: 'local_temperature',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: '°C',
|
||||
device_class: 'temperature',
|
||||
value_template: '{{ value_json.local_temperature }}',
|
||||
},
|
||||
},
|
||||
'sensor_pressure': {
|
||||
type: 'sensor',
|
||||
object_id: 'pressure',
|
||||
@@ -227,7 +263,8 @@ const cfg = {
|
||||
type: 'sensor',
|
||||
object_id: 'linkquality',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: '-',
|
||||
icon: 'mdi:signal',
|
||||
unit_of_measurement: 'lqi',
|
||||
value_template: '{{ value_json.linkquality }}',
|
||||
},
|
||||
},
|
||||
@@ -255,6 +292,15 @@ const cfg = {
|
||||
icon: 'mdi:view-array',
|
||||
},
|
||||
},
|
||||
'sensor_consumption': {
|
||||
type: 'sensor',
|
||||
object_id: 'consumption',
|
||||
discovery_payload: {
|
||||
unit_of_measurement: 'kWh',
|
||||
value_template: '{{ value_json.consumption }}',
|
||||
icon: 'mdi:flash',
|
||||
},
|
||||
},
|
||||
'sensor_sensitivity': {
|
||||
type: 'sensor',
|
||||
object_id: 'sensitivity',
|
||||
@@ -263,6 +309,14 @@ const cfg = {
|
||||
icon: 'mdi:filter-variant',
|
||||
},
|
||||
},
|
||||
'sensor_strength': {
|
||||
type: 'sensor',
|
||||
object_id: 'strength',
|
||||
discovery_payload: {
|
||||
value_template: '{{ value_json.strength }}',
|
||||
icon: 'mdi:weight',
|
||||
},
|
||||
},
|
||||
|
||||
// Light
|
||||
'light_brightness_colorxy_white': {
|
||||
@@ -276,6 +330,18 @@ const cfg = {
|
||||
command_topic: true,
|
||||
},
|
||||
},
|
||||
'light_brightness_colortemp_colorxy_white': {
|
||||
type: 'light',
|
||||
object_id: 'light',
|
||||
discovery_payload: {
|
||||
brightness: true,
|
||||
xy: true,
|
||||
white_value: true,
|
||||
schema: 'json',
|
||||
command_topic: true,
|
||||
color_temp: true,
|
||||
},
|
||||
},
|
||||
'light_brightness_colortemp_colorxy': {
|
||||
type: 'light',
|
||||
object_id: 'light',
|
||||
@@ -297,6 +363,17 @@ const cfg = {
|
||||
command_topic: true,
|
||||
},
|
||||
},
|
||||
'light_brightness_colortemp_colorhs': {
|
||||
type: 'light',
|
||||
object_id: 'light',
|
||||
discovery_payload: {
|
||||
brightness: true,
|
||||
color_temp: true,
|
||||
hs: true,
|
||||
schema: 'json',
|
||||
command_topic: true,
|
||||
},
|
||||
},
|
||||
'light_brightness_colortemp': {
|
||||
type: 'light',
|
||||
object_id: 'light',
|
||||
@@ -373,11 +450,25 @@ const cfg = {
|
||||
discovery_payload: {
|
||||
command_topic: true,
|
||||
value_template: '{{ value_json.state }}',
|
||||
state_locked: 'LOCK',
|
||||
state_unlocked: 'UNLOCK',
|
||||
},
|
||||
},
|
||||
'lock_keypad_lockout': {
|
||||
type: 'lock',
|
||||
object_id: 'keypad_lock',
|
||||
discovery_payload: {
|
||||
state_topic: true,
|
||||
command_topic: true,
|
||||
command_topic_postfix: 'keypad_lockout',
|
||||
payload_unlock: '0',
|
||||
payload_lock: '1',
|
||||
value_template: '{{ value_json.keypad_lockout }}',
|
||||
},
|
||||
},
|
||||
|
||||
// Thermostat/HVAC
|
||||
'thermostat': (minTemp=7, maxTemp=30, temperatureStateProperty='occupied_heating_setpoint') => {
|
||||
'thermostat': (minTemp=7, maxTemp=30, temperatureStateProperty='occupied_heating_setpoint', tempStep=1) => {
|
||||
return {
|
||||
type: 'climate',
|
||||
object_id: 'climate',
|
||||
@@ -394,6 +485,9 @@ const cfg = {
|
||||
temperature_state_topic: true,
|
||||
temperature_state_template: `{{ value_json.${temperatureStateProperty} }}`,
|
||||
temperature_command_topic: temperatureStateProperty,
|
||||
temp_step: tempStep,
|
||||
action_topic: true,
|
||||
action_template: '{{ value_json.operation }}',
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -432,7 +526,7 @@ const switchWithPostfix = (postfix) => {
|
||||
// Map homeassitant configurations to devices.
|
||||
const mapping = {
|
||||
'WXKG01LM': [cfg.sensor_click, cfg.sensor_battery],
|
||||
'WXKG11LM': [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],
|
||||
@@ -457,6 +551,7 @@ const mapping = {
|
||||
'LED1536G5': [cfg.light_brightness_colortemp],
|
||||
'7299760PH': [cfg.light_brightness_colorxy],
|
||||
'7146060PH': [cfg.light_brightness_colortemp_colorxy],
|
||||
'7602031P7': [cfg.light_brightness_colortemp_colorxy],
|
||||
'046677476816': [cfg.light_brightness],
|
||||
'F7C033': [cfg.light_brightness],
|
||||
'JTYJ-GD-01LM/BW': [cfg.binary_sensor_smoke, cfg.sensor_battery, cfg.sensor_sensitivity, cfg.sensor_smoke_density],
|
||||
@@ -464,6 +559,7 @@ const mapping = {
|
||||
'SWITCH EDP RE:DY': [cfg.switch],
|
||||
'CC2530.ROUTER': [cfg.binary_sensor_router],
|
||||
'AA70155': [cfg.light_brightness_colortemp],
|
||||
'A9A19A60WESDZ02': [cfg.light_brightness_colortemp],
|
||||
'4058075816718': [cfg.light_brightness_colortemp_colorxy],
|
||||
'AA69697': [cfg.light_brightness_colortemp_colorxy],
|
||||
'HALIGHTDIMWWE27': [cfg.light_brightness],
|
||||
@@ -479,6 +575,8 @@ const mapping = {
|
||||
'73742': [cfg.light_brightness_colortemp],
|
||||
'73740': [cfg.light_brightness_colortemp],
|
||||
'73739': [cfg.light_brightness_colortemp_colorxy],
|
||||
'72569': [cfg.light_brightness_colortemp],
|
||||
'72567': [cfg.light_brightness_colortemp],
|
||||
'22670': [cfg.light_brightness],
|
||||
'ICTC-G-1': [cfg.sensor_brightness, cfg.sensor_battery, cfg.sensor_action],
|
||||
'ICPSHC24-30EU-IL-1': [cfg.light_brightness],
|
||||
@@ -513,26 +611,32 @@ const mapping = {
|
||||
'DNCKATSW001': [cfg.switch],
|
||||
'Z809A': [cfg.switch, cfg.sensor_power],
|
||||
'NL08-0800': [cfg.light_brightness],
|
||||
'98425031': [cfg.light_brightness],
|
||||
'915005106701': [cfg.light_brightness_colortemp_colorxy],
|
||||
'Aj_Zigbee_Led_Strip': [cfg.light_brightness_colortemp_colorxy],
|
||||
'AB32840': [cfg.light_brightness_colortemp],
|
||||
'8718696485880': [cfg.light_brightness_colortemp_colorxy],
|
||||
'8718696598283': [cfg.light_brightness_colortemp],
|
||||
'8718696695203': [cfg.light_brightness_colortemp],
|
||||
'73693': [cfg.light_brightness_colortemp_colorxy],
|
||||
'324131092621': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'9290012607': [cfg.binary_sensor_occupancy, cfg.sensor_temperature, cfg.sensor_illuminance, cfg.sensor_battery],
|
||||
'9290012607': [
|
||||
cfg.binary_sensor_occupancy, cfg.sensor_temperature, cfg.sensor_illuminance, cfg.sensor_illuminance_lux,
|
||||
cfg.sensor_battery,
|
||||
],
|
||||
'GL-C-008': [cfg.light_brightness_colortemp_colorxy],
|
||||
'GL-C-009': [cfg.light_brightness],
|
||||
'STSS-MULT-001': [cfg.binary_sensor_contact, cfg.sensor_battery],
|
||||
'E11-G23/E11-G33': [cfg.light_brightness],
|
||||
'E1ACA4ABE38A': [cfg.light_brightness],
|
||||
'AC03645': [cfg.light_brightness_colortemp_colorxy],
|
||||
'AC03645': [cfg.light_brightness_colortemp_colorhs],
|
||||
'AC03641': [cfg.light_brightness],
|
||||
'AC03648': [cfg.light_brightness_colortemp],
|
||||
'FB56+ZSW05HG1.2': [cfg.switch],
|
||||
'72922-A': [cfg.switch],
|
||||
'AC03642': [cfg.light_brightness_colortemp],
|
||||
'AC08560': [cfg.light_brightness],
|
||||
'AC10786-DIM': [cfg.light_brightness],
|
||||
'DNCKATSW002': [switchWithPostfix('left'), switchWithPostfix('right')],
|
||||
'DNCKATSW003': [switchWithPostfix('left'), switchWithPostfix('right'), switchWithPostfix('center')],
|
||||
'DNCKATSW004': [
|
||||
@@ -543,13 +647,15 @@ const mapping = {
|
||||
'ZLED-2709': [cfg.light_brightness],
|
||||
'8718696548738': [cfg.light_brightness_colortemp],
|
||||
'915005587401': [cfg.light_brightness_colortemp],
|
||||
'3435011P7': [cfg.light_brightness_colortemp],
|
||||
'4052899926110': [cfg.light_brightness_colortemp_colorxy],
|
||||
'Z01-CIA19NAE26': [cfg.light_brightness],
|
||||
'E11-N1EA': [cfg.light_brightness_colortemp_colorxy],
|
||||
'E11-U2E': [cfg.light_brightness_colortemp_colorxy],
|
||||
'74283': [cfg.light_brightness],
|
||||
'JTQJ-BF-01LM/BW': [cfg.binary_sensor_gas, cfg.sensor_gas_density, cfg.sensor_sensitivity],
|
||||
'50043': [cfg.switch],
|
||||
'50045': [cfg.light_brightness],
|
||||
'50044/50045': [cfg.light_brightness],
|
||||
'AV2010/22': [cfg.binary_sensor_occupancy, cfg.sensor_battery],
|
||||
'3210-L': [cfg.switch, cfg.sensor_power, cfg.sensor_current, cfg.sensor_voltage],
|
||||
'3320-L': [cfg.binary_sensor_contact, cfg.sensor_temperature, cfg.sensor_battery],
|
||||
@@ -560,7 +666,7 @@ const mapping = {
|
||||
'433714': [cfg.light_brightness],
|
||||
'3261030P7': [cfg.light_brightness_colortemp],
|
||||
'3216431P5': [cfg.light_brightness_colortemp],
|
||||
'DJT11LM': [cfg.sensor_action, cfg.sensor_battery, cfg.sensor_sensitivity],
|
||||
'DJT11LM': [cfg.sensor_action, cfg.sensor_battery, cfg.sensor_sensitivity, cfg.sensor_strength],
|
||||
'E1603/E1702': [cfg.switch],
|
||||
'7199960PH': [cfg.light_brightness_colorxy],
|
||||
'74696': [cfg.light_brightness],
|
||||
@@ -587,10 +693,13 @@ const mapping = {
|
||||
'900008-WW': [cfg.light_brightness],
|
||||
'Mega23M12': [cfg.light_brightness_colortemp_colorxy],
|
||||
'PSS-23ZBS': [cfg.switch],
|
||||
'HS1SA': [cfg.binary_sensor_smoke, cfg.binary_sensor_battery_low],
|
||||
'HS1SA-M': [cfg.binary_sensor_smoke, cfg.binary_sensor_battery_low],
|
||||
'Z01-A19NAE26': [cfg.light_brightness_colortemp],
|
||||
'Z01-A60EAE27': [cfg.light_brightness_colortemp],
|
||||
'AC01353010G': [cfg.binary_sensor_occupancy, cfg.sensor_temperature, cfg.binary_sensor_battery_low],
|
||||
'AC01353010G': [
|
||||
cfg.binary_sensor_occupancy, cfg.binary_sensor_tamper,
|
||||
cfg.sensor_temperature, cfg.binary_sensor_battery_low,
|
||||
],
|
||||
'SP 120': [cfg.switch, cfg.sensor_power],
|
||||
'SP 222': [cfg.switch],
|
||||
'RB 248 T': [cfg.light_brightness_colortemp],
|
||||
@@ -601,18 +710,19 @@ const mapping = {
|
||||
'421786': [cfg.light_brightness],
|
||||
'ICZB-IW11D': [cfg.light_brightness],
|
||||
'3321-S': [cfg.binary_sensor_contact, cfg.sensor_temperature],
|
||||
'ZPIR-8000': [cfg.binary_sensor_occupancy],
|
||||
'ZPIR-8000': [cfg.binary_sensor_occupancy, cfg.sensor_battery],
|
||||
'ZCTS-808': [cfg.binary_sensor_contact, cfg.sensor_battery],
|
||||
'ZNLDP12LM': [cfg.light_brightness_colortemp],
|
||||
'D1821': [cfg.light_brightness_colortemp_colorxy],
|
||||
'ZNCLDJ11LM': [cfg.cover_position, cfg.sensor_cover],
|
||||
'LTFY004': [cfg.light_brightness_colorxy],
|
||||
'GL-S-007Z': [cfg.light_brightness_colorxy_white],
|
||||
'GL-S-007Z': [cfg.light_brightness_colortemp_colorxy],
|
||||
'3325-S': [cfg.sensor_temperature, cfg.binary_sensor_occupancy],
|
||||
'4713407': [cfg.light_brightness],
|
||||
'464800': [cfg.light_brightness_colortemp],
|
||||
'3261331P7': [cfg.light_brightness_colortemp],
|
||||
'4033930P7': [cfg.light_brightness_colortemp],
|
||||
'4023330P7': [cfg.light_brightness_colortemp],
|
||||
'GL-B-008Z': [cfg.light_brightness_colortemp_colorxy],
|
||||
'AV2010/25': [cfg.switch, cfg.sensor_power],
|
||||
'E12-N14': [cfg.light_brightness],
|
||||
@@ -626,18 +736,23 @@ const mapping = {
|
||||
'HGZB-20-DE': [cfg.switch],
|
||||
'D1531': [cfg.light_brightness],
|
||||
'D1532': [cfg.light_brightness],
|
||||
'D1533': [cfg.light_brightness],
|
||||
'AV2010/32': [],
|
||||
'HGZB-07A': [cfg.light_brightness_colortemp_colorxy],
|
||||
'E1524/E1810': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'GL-C-006': [cfg.light_brightness_colortemp],
|
||||
'GL-C-007': [cfg.light_brightness_colorxy_white],
|
||||
'GL-C-007S': [cfg.light_brightness_colorxy_white],
|
||||
'GL-C-007/GL-C-008': [cfg.light_brightness_colortemp_colorxy_white],
|
||||
'100.424.11': [cfg.light_brightness_colortemp],
|
||||
'AC0251100NJ': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'AC0251100NJ/AC0251700NJ': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'71831': [cfg.light_brightness_colortemp],
|
||||
'404000/404005/404012': [cfg.light_brightness_colortemp_colorxy],
|
||||
'44435': [cfg.light_brightness_colortemp_colorxy],
|
||||
'404006/404008/404004': [cfg.light_brightness_colortemp],
|
||||
'MLI-404011': [cfg.sensor_action],
|
||||
'GL-S-003Z': [cfg.light_brightness_colorxy_white],
|
||||
'GL-S-005Z': [cfg.light_brightness_colortemp_colorxy],
|
||||
'HS1DS-E': [cfg.binary_sensor_contact],
|
||||
'SP600': [cfg.switch, cfg.sensor_power],
|
||||
'1613V': [cfg.switch, cfg.sensor_power],
|
||||
@@ -647,14 +762,15 @@ const mapping = {
|
||||
'4090130P7': [cfg.light_brightness_colortemp_colorxy],
|
||||
'100.110.39': [cfg.light_brightness_colortemp_colorxy],
|
||||
'TI0001': [switchWithPostfix('left'), switchWithPostfix('right')],
|
||||
'SPZB0001': [cfg.thermostat(5, 30, 'current_heating_setpoint'), cfg.sensor_battery],
|
||||
'SPZB0001': [cfg.thermostat(5, 30, 'current_heating_setpoint', 0.5), cfg.sensor_battery],
|
||||
'HS3CG': [cfg.binary_sensor_gas],
|
||||
'81825': [cfg.sensor_action],
|
||||
'Z809AF': [cfg.switch, cfg.sensor_power],
|
||||
'RADON TriTech ZB': [
|
||||
cfg.binary_sensor_occupancy, cfg.sensor_temperature, cfg.sensor_battery, cfg.binary_sensor_battery_low,
|
||||
],
|
||||
'IM-Z3.0-DIM': [cfg.light_brightness],
|
||||
'07005B': [cfg.light_brightness],
|
||||
'07004D': [cfg.light_brightness_colortemp_colorxy],
|
||||
'E1746': [],
|
||||
'LED1836G9': [cfg.light_brightness],
|
||||
'YRD426NRSC': [cfg.lock, cfg.sensor_battery],
|
||||
@@ -664,7 +780,7 @@ const mapping = {
|
||||
'RB 265': [cfg.light_brightness],
|
||||
'9290019758': [
|
||||
cfg.binary_sensor_occupancy, cfg.sensor_temperature,
|
||||
cfg.sensor_illuminance, cfg.sensor_battery,
|
||||
cfg.sensor_illuminance, cfg.sensor_illuminance_lux, cfg.sensor_battery,
|
||||
],
|
||||
'HGZB-042': [switchWithPostfix('top'), switchWithPostfix('bottom')],
|
||||
'HGZB-42': [switchWithPostfix('top'), switchWithPostfix('bottom')],
|
||||
@@ -681,11 +797,11 @@ const mapping = {
|
||||
'316GLEDRF': [cfg.light_brightness],
|
||||
'LVS-ZB500D': [cfg.light_brightness],
|
||||
'ST218': [],
|
||||
'E1525': [cfg.binary_sensor_occupancy, cfg.sensor_battery],
|
||||
'E1525/E1745': [cfg.binary_sensor_occupancy, cfg.sensor_battery],
|
||||
'ZYCT-202': [cfg.sensor_action],
|
||||
'GR-ZB01-W': [cfg.cover_position],
|
||||
'4090531P7': [cfg.light_brightness_colortemp_colorxy],
|
||||
'HGZB-42-UK / HGZB-41': [cfg.switch],
|
||||
'HGZB-42-UK / HGZB-41 / HGZB-41-UK': [cfg.switch],
|
||||
'ISW-ZPR1-WP13': [
|
||||
cfg.binary_sensor_occupancy, cfg.sensor_temperature,
|
||||
cfg.sensor_battery, cfg.binary_sensor_battery_low,
|
||||
@@ -717,7 +833,7 @@ 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],
|
||||
'IM6001-BTP01': [cfg.sensor_click, cfg.sensor_temperature, cfg.sensor_battery],
|
||||
'AV2010/34': [cfg.sensor_click],
|
||||
'PP-WHT-US': [
|
||||
cfg.switch, cfg.sensor_power,
|
||||
@@ -759,7 +875,11 @@ const mapping = {
|
||||
'BY 285 C': [cfg.light_brightness_colortemp_colorxy],
|
||||
'HS1RC-M': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'SWO-WDS1PA': [cfg.binary_sensor_contact],
|
||||
'LLKZMK11LM': [switchWithPostfix('l1'), switchWithPostfix('l2')],
|
||||
'LLKZMK11LM': [
|
||||
switchWithPostfix('l1'), switchWithPostfix('l2'),
|
||||
cfg.sensor_power, cfg.sensor_temperature, cfg.sensor_consumption,
|
||||
],
|
||||
'T18W3Z': [switchWithPostfix('l1'), switchWithPostfix('l2'), switchWithPostfix('l3')],
|
||||
'LVS-SM10ZW': [cfg.binary_sensor_contact, cfg.binary_sensor_battery_low],
|
||||
'HS2SK': [cfg.switch, cfg.sensor_power],
|
||||
'45853GE': [cfg.switch, cfg.sensor_power],
|
||||
@@ -767,7 +887,7 @@ const mapping = {
|
||||
'9290011998B': [cfg.light_brightness_colortemp],
|
||||
'9290022167': [cfg.light_brightness_colortemp],
|
||||
'4096730U7': [cfg.light_brightness_colortemp],
|
||||
'RB 278 T': [cfg.light_brightness],
|
||||
'RB 278 T': [cfg.light_brightness_colortemp],
|
||||
'3315-G': [
|
||||
cfg.sensor_temperature, cfg.sensor_battery, cfg.binary_sensor_water_leak,
|
||||
],
|
||||
@@ -826,8 +946,9 @@ const mapping = {
|
||||
'7099860PH': [cfg.light_brightness_colorxy],
|
||||
'HV-GSCXZB269': [cfg.light_brightness_colortemp],
|
||||
'3216231P5': [cfg.light_brightness_colortemp],
|
||||
'AC03647': [cfg.light_brightness_colortemp_colorxy],
|
||||
'AC03647': [cfg.light_brightness_colortemp_colorhs],
|
||||
'12031': [cfg.cover_position],
|
||||
'LS12128': [cfg.cover_position],
|
||||
'421792': [cfg.light_brightness_colortemp_colorxy],
|
||||
'HGZB-06A': [cfg.light_brightness_colortemp_colorxy],
|
||||
'LED1733G7': [cfg.light_brightness_colortemp],
|
||||
@@ -843,8 +964,12 @@ const mapping = {
|
||||
'ZNMS13LM': [
|
||||
cfg.sensor_action, cfg.binary_sensor_lock, cfg.binary_sensor_lock_reverse,
|
||||
],
|
||||
'ZNMS11LM': [
|
||||
cfg.sensor_action, cfg.binary_sensor_lock, cfg.binary_sensor_lock_reverse,
|
||||
],
|
||||
'12050': [cfg.switch, cfg.sensor_power],
|
||||
'ROB_200-004-0': [cfg.light_brightness],
|
||||
'4512700': [cfg.light_brightness],
|
||||
'RH3040': [cfg.sensor_battery, cfg.binary_sensor_occupancy],
|
||||
'DZ4743-00B': [cfg.switch],
|
||||
'GLSK3ZB-1711': [cfg.switch],
|
||||
@@ -863,7 +988,7 @@ const mapping = {
|
||||
switchWithPostfix('top_right'), switchWithPostfix('center_right'), switchWithPostfix('bottom_right'),
|
||||
],
|
||||
'3306431P7': [cfg.light_brightness_colortemp],
|
||||
'AC08559': [cfg.light_brightness_colortemp],
|
||||
'AC08559': [cfg.light_brightness_colortemp_colorxy],
|
||||
'LVS-ZB15S': [cfg.switch],
|
||||
'LZL4BWHL01': [cfg.sensor_action],
|
||||
'2AJZ4KPKEY': [cfg.sensor_click, cfg.sensor_battery],
|
||||
@@ -872,10 +997,18 @@ const mapping = {
|
||||
'ZM-CSW002-D': [switchWithPostfix('l1'), switchWithPostfix('l2'), cfg.sensor_power],
|
||||
'LVS-SN10ZW': [cfg.sensor_battery, cfg.binary_sensor_occupancy],
|
||||
'LVS-ZB15R': [cfg.switch],
|
||||
'TH1123ZB': [cfg.thermostat()],
|
||||
'TH1123ZB': [
|
||||
cfg.thermostat(7, 30, 'occupied_heating_setpoint', 1.0), cfg.sensor_local_temperature,
|
||||
cfg.lock_keypad_lockout, cfg.sensor_power,
|
||||
],
|
||||
'TH1124ZB': [cfg.thermostat()],
|
||||
'TH1400ZB': [cfg.thermostat()],
|
||||
'TH1500ZB': [cfg.thermostat()],
|
||||
'Zen-01-W': [cfg.thermostat()],
|
||||
'9290022166': [cfg.light_brightness_colortemp_colorxy],
|
||||
'PM-C140-ZB': [cfg.sensor_power, cfg.switch],
|
||||
'PM-B530-ZB': [cfg.sensor_power, cfg.switch],
|
||||
'PM-B430-ZB': [cfg.sensor_power, cfg.switch],
|
||||
'ptvo.switch': [
|
||||
switchWithPostfix('bottom_left'), switchWithPostfix('bottom_right'), switchWithPostfix('top_left'),
|
||||
switchWithPostfix('top_right'), switchWithPostfix('center'), cfg.sensor_click,
|
||||
@@ -886,26 +1019,25 @@ const mapping = {
|
||||
],
|
||||
'DIYRuZ_KEYPAD20': [],
|
||||
'DTB190502A1': [],
|
||||
'FL 130 C': [cfg.light_brightness_colortemp_colorxy],
|
||||
'BF 263': [cfg.light_brightness],
|
||||
'RF 263': [cfg.light_brightness],
|
||||
'F-WTR-UK-V2': [
|
||||
cfg.sensor_temperature, cfg.binary_sensor_water_leak, cfg.sensor_battery,
|
||||
],
|
||||
'HS1CG-M': [cfg.binary_sensor_gas],
|
||||
'HS1CG_M': [cfg.binary_sensor_gas],
|
||||
'LVS-SN10ZW_SN11': [cfg.sensor_battery, cfg.binary_sensor_occupancy],
|
||||
'B00TN589ZG': [cfg.light_brightness],
|
||||
'PSB19-SW27': [cfg.light_brightness],
|
||||
'S1': [cfg.switch],
|
||||
'S2': [switchWithPostfix('l1'), switchWithPostfix('l2')],
|
||||
'S1': [cfg.switch, cfg.sensor_power],
|
||||
'S2': [switchWithPostfix('l1'), switchWithPostfix('l2'), cfg.sensor_power],
|
||||
'ZWallRemote0': [cfg.sensor_click],
|
||||
'D1': [cfg.light_brightness],
|
||||
'J1': [cfg.cover_position_tilt],
|
||||
'D1': [cfg.light_brightness, cfg.sensor_power],
|
||||
'J1': [cfg.cover_position_tilt, cfg.sensor_power],
|
||||
'73741': [cfg.light_brightness_colortemp_colorxy],
|
||||
'ZA806SQ1TCF': [cfg.light_brightness_colortemp],
|
||||
'RF 265': [cfg.light_brightness],
|
||||
'ZNCZ03LM': [cfg.switch, cfg.sensor_power],
|
||||
'17436/30/P7': [cfg.light_brightness],
|
||||
'17435/30/P7': [cfg.light_brightness_colorxy],
|
||||
'9290018187B': [cfg.light_brightness_colortemp_colorxy],
|
||||
'1741830P7': [cfg.light_brightness_colortemp_colorxy],
|
||||
'Z3-1BRL': [cfg.sensor_action, cfg.sensor_brightness],
|
||||
@@ -920,7 +1052,7 @@ const mapping = {
|
||||
'2AJZ4KPBS': [cfg.sensor_battery, cfg.binary_sensor_occupancy, cfg.binary_sensor_battery_low],
|
||||
'2AJZ4KPDR': [cfg.sensor_battery, cfg.binary_sensor_contact, cfg.binary_sensor_battery_low],
|
||||
'6717-84': [cfg.switch],
|
||||
'ICZB-KPD18S': [cfg.sensor_click],
|
||||
'ICZB-KPD18S': [cfg.sensor_battery, cfg.sensor_click, cfg.sensor_action],
|
||||
'100.469.65': [cfg.light_brightness_colortemp],
|
||||
'E1757': [cfg.cover_position, cfg.sensor_battery],
|
||||
'E1926': [cfg.cover_position, cfg.sensor_battery],
|
||||
@@ -933,6 +1065,7 @@ const mapping = {
|
||||
'8718699673147': [cfg.light_brightness],
|
||||
'3300-P': [cfg.sensor_temperature, cfg.binary_sensor_contact, cfg.sensor_battery],
|
||||
'GL-B-008ZS': [cfg.light_brightness_colortemp_colorxy],
|
||||
'T1828': [cfg.light_brightness_colortemp],
|
||||
'T1829': [cfg.light_brightness_colortemp],
|
||||
'929002240401': [cfg.switch],
|
||||
'HGZB-20-UK': [cfg.switch],
|
||||
@@ -945,18 +1078,19 @@ const mapping = {
|
||||
'9290020399': [cfg.light_brightness],
|
||||
'929002241201': [cfg.light_brightness],
|
||||
'YRD210-HA-605': [cfg.lock, cfg.sensor_battery],
|
||||
'YRD220/240 TSDB': [cfg.lock, cfg.sensor_battery],
|
||||
'ZM-CSW032-D': [cfg.cover],
|
||||
'LH-32ZB': [cfg.sensor_humidity, cfg.sensor_temperature, cfg.sensor_battery],
|
||||
'511.201': [cfg.light_brightness],
|
||||
'ZNCLDJ12LM': [cfg.cover_position],
|
||||
'046677552343': [cfg.switch],
|
||||
'3115331PH': [cfg.light_brightness_colortemp_colorxy],
|
||||
'ZWLD-100': [cfg.binary_sensor_water_leak, cfg.binary_sensor_battery_low],
|
||||
'ZWLD-100': [cfg.binary_sensor_water_leak, cfg.binary_sensor_battery_low, cfg.sensor_battery],
|
||||
'GL-MC-001': [cfg.light_brightness_colortemp_colorxy],
|
||||
'YRD226/246 TSDB': [cfg.lock, cfg.sensor_battery],
|
||||
'T1820': [cfg.light_brightness_colortemp],
|
||||
'BASICZBR3': [cfg.switch],
|
||||
'E1744': [cfg.sensor_action],
|
||||
'E1744': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'TS0201': [cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_battery],
|
||||
'LH07321': [cfg.binary_sensor_water_leak, cfg.binary_sensor_battery_low],
|
||||
'GL-C-008S': [cfg.light_brightness_colortemp_colorxy],
|
||||
@@ -964,9 +1098,163 @@ const mapping = {
|
||||
'8718699688882': [cfg.light_brightness],
|
||||
'LED1738G7': [cfg.light_brightness_colortemp],
|
||||
'9290022169': [cfg.light_brightness_colortemp],
|
||||
'TERNCY-PP01': [cfg.sensor_temperature, cfg.binary_sensor_occupancy, cfg.sensor_illuminance, cfg.sensor_click],
|
||||
'TERNCY-PP01': [
|
||||
cfg.sensor_temperature, cfg.binary_sensor_occupancy, cfg.sensor_illuminance, cfg.sensor_illuminance_lux,
|
||||
cfg.sensor_click,
|
||||
],
|
||||
'CR11S8UZ': [cfg.sensor_action],
|
||||
'RB 148 T': [cfg.light_brightness_colortemp],
|
||||
'STS-OUT-US-2': [cfg.switch, cfg.sensor_power],
|
||||
'UK7004240': [cfg.thermostat(), cfg.sensor_battery],
|
||||
'S31ZB': [cfg.switch],
|
||||
'SA-003-Zigbee': [cfg.switch],
|
||||
'SZ-DWS04': [cfg.binary_sensor_contact, cfg.sensor_temperature, cfg.sensor_battery],
|
||||
'ICZB-B1FC60/B3FC64/B2FC95/B2FC125': [cfg.light_brightness_colortemp],
|
||||
'TS0203': [cfg.sensor_battery, cfg.binary_sensor_contact],
|
||||
'TS0204': [cfg.binary_sensor_gas],
|
||||
'TS0205': [cfg.binary_sensor_smoke, cfg.sensor_battery],
|
||||
'TS0111': [cfg.switch],
|
||||
'TS0001': [cfg.switch],
|
||||
'TS0207': [cfg.binary_sensor_water_leak, cfg.sensor_battery],
|
||||
'iL07_1': [cfg.binary_sensor_occupancy, cfg.binary_sensor_tamper, cfg.binary_sensor_battery_low],
|
||||
'S31 Lite zb': [cfg.switch],
|
||||
'LH-992ZB': [cfg.binary_sensor_occupancy, cfg.binary_sensor_battery_low],
|
||||
'548727': [cfg.light_brightness_colortemp_colorxy],
|
||||
'TS0202': [cfg.binary_sensor_occupancy, cfg.sensor_battery],
|
||||
'TS0218': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'404021': [cfg.switch],
|
||||
'Eco-Dim.07': [cfg.light_brightness],
|
||||
'DIYRuZ_rspm': [cfg.switch, cfg.sensor_action, cfg.sensor_power, cfg.sensor_current],
|
||||
'ZG9101SAC-HP-Switch': [cfg.switch],
|
||||
'ZNCZ04LM': [cfg.switch, cfg.sensor_power],
|
||||
'ZNCZ12LM': [cfg.switch, cfg.sensor_power],
|
||||
'GL-S-007ZS': [cfg.light_brightness_colortemp_colorxy],
|
||||
'4058075816732': [cfg.light_brightness_colortemp_colorxy],
|
||||
'GL-B-007ZS': [cfg.light_brightness_colortemp_colorxy],
|
||||
'GL-G-007Z': [cfg.light_brightness_colortemp_colorxy],
|
||||
'WXCJKG11LM': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'WXCJKG12LM': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'WXCJKG13LM': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'8718699693985': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'GL-D-004ZS': [cfg.light_brightness_colortemp_colorxy],
|
||||
'AC10787': [cfg.light_brightness_colortemp],
|
||||
'F-APP-UK-V2': [cfg.switch],
|
||||
'S9TSZGB': [cfg.sensor_action],
|
||||
'SP-EUC01': [cfg.switch],
|
||||
'511.012': [cfg.light_brightness],
|
||||
'GL-S-008Z': [cfg.light_brightness_colortemp_colorxy],
|
||||
'TZSW22FW-L4': [switchWithPostfix('top'), switchWithPostfix('bottom')],
|
||||
'GDKES-01TZXD': [cfg.switch],
|
||||
'GDKES-02TZXD': [switchWithPostfix('left'), switchWithPostfix('right')],
|
||||
'GDKES-03TZXD': [switchWithPostfix('left'), switchWithPostfix('center'), switchWithPostfix('right')],
|
||||
'6735/6736/6737': [cfg.switch, cfg.sensor_action],
|
||||
'4034031P7': [cfg.light_brightness_colortemp],
|
||||
'5900131C5': [cfg.light_brightness_colortemp],
|
||||
'SZ-SRN12N': [],
|
||||
'ML-ST-D200': [cfg.light_brightness],
|
||||
'7099930PH': [cfg.light_brightness_colorxy],
|
||||
'9GED18000-009': [cfg.lock, cfg.sensor_battery],
|
||||
'9GED21500-005': [cfg.lock, cfg.sensor_battery],
|
||||
'MP-841': [cfg.binary_sensor_occupancy, cfg.binary_sensor_battery_low],
|
||||
'MCT-370 SMA': [cfg.binary_sensor_contact, cfg.binary_sensor_battery_low],
|
||||
'RB 162': [cfg.light_brightness],
|
||||
'SOHM-I1': [cfg.binary_sensor_contact, cfg.binary_sensor_battery_low],
|
||||
'SWHM-I1': [cfg.binary_sensor_water_leak, cfg.binary_sensor_battery_low],
|
||||
'SMHM-I1': [cfg.binary_sensor_occupancy, cfg.binary_sensor_battery_low],
|
||||
'SKHMP30-I1': [cfg.switch, cfg.sensor_power],
|
||||
'404028': [cfg.light_brightness_colortemp_colorxy],
|
||||
'595UGR22': [cfg.light_brightness_colortemp],
|
||||
'6ARCZABZH': [cfg.sensor_battery, cfg.sensor_action],
|
||||
'ZK-EU-2U': [cfg.switch],
|
||||
'511.202': [cfg.switch],
|
||||
'SP 224': [cfg.switch],
|
||||
'9290022411': [cfg.light_brightness],
|
||||
'E1C-NB6': [cfg.switch],
|
||||
'LVS-SC7': [cfg.sensor_action],
|
||||
'1742930P7': [cfg.light_brightness_colortemp_colorxy],
|
||||
'ZM-L03E-Z': [switchWithPostfix('left'), switchWithPostfix('center'), switchWithPostfix('right')],
|
||||
'DL15S-1BZ': [cfg.switch],
|
||||
'E1D-G73WNA': [cfg.binary_sensor_contact, cfg.binary_sensor_battery_low],
|
||||
'WV704R0A0902': [cfg.thermostat()],
|
||||
'067776': [cfg.cover_position],
|
||||
'067773': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'067771': [cfg.switch],
|
||||
'064873': [cfg.sensor_action],
|
||||
'K4003C': [cfg.switch],
|
||||
'STZB402': [
|
||||
cfg.thermostat(5, 30, 'occupied_heating_setpoint', 0.5),
|
||||
cfg.sensor_local_temperature,
|
||||
cfg.lock_keypad_lockout,
|
||||
],
|
||||
'SMT402': [
|
||||
cfg.thermostat(5, 30, 'occupied_heating_setpoint', 0.5),
|
||||
cfg.sensor_local_temperature,
|
||||
cfg.lock_keypad_lockout,
|
||||
],
|
||||
'046677551780': [cfg.light_brightness],
|
||||
'798.15': [cfg.light_brightness],
|
||||
'12126': [cfg.switch],
|
||||
'067775': [cfg.switch, cfg.sensor_power],
|
||||
'064888': [cfg.switch],
|
||||
'gq8b1uv': [cfg.light_brightness],
|
||||
'GZCGQ01LM': [cfg.sensor_battery, cfg.sensor_illuminance, cfg.sensor_illuminance_lux],
|
||||
'9290018215': [cfg.light_brightness],
|
||||
'1743230P7': [cfg.light_brightness_colortemp_colorxy],
|
||||
'100.110.51': [cfg.light_brightness_colortemp],
|
||||
'ZL1000100-CCT-US-V1A02': [cfg.light_brightness],
|
||||
'HGZB-DLC4-N12B': [cfg.light_brightness_colortemp_colorxy],
|
||||
'U86KCJ-ZP': [cfg.sensor_action],
|
||||
'HS1HT': [cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_battery],
|
||||
'HS2ESK-E': [cfg.switch, cfg.sensor_power],
|
||||
'B01M7Y8BP9': [cfg.sensor_action],
|
||||
'GP-WOU019BBDWG': [cfg.switch, cfg.sensor_power],
|
||||
'AV2010/21A': [cfg.binary_sensor_battery_low, cfg.binary_sensor_contact, cfg.binary_sensor_tamper],
|
||||
'AL-PIR02': [cfg.binary_sensor_occupancy, cfg.binary_sensor_tamper, cfg.sensor_battery],
|
||||
'MKS-CM-W5': [
|
||||
switchWithPostfix('l1'), switchWithPostfix('l2'),
|
||||
switchWithPostfix('l3'), switchWithPostfix('l4'),
|
||||
],
|
||||
'STS-WTR-250': [cfg.binary_sensor_water_leak, cfg.sensor_battery],
|
||||
'ZG2835RAC': [cfg.light_brightness],
|
||||
'BW-IS2': [cfg.binary_sensor_contact, cfg.sensor_battery],
|
||||
'BW-IS3': [cfg.binary_sensor_occupancy],
|
||||
'SLR1b': [cfg.thermostat()],
|
||||
'WPT1': [],
|
||||
'4058075047853': [cfg.light_brightness_colortemp_colorxy],
|
||||
'ROB_200-003-0': [cfg.switch],
|
||||
'4512704': [cfg.switch],
|
||||
'AV2010/24A': [cfg.binary_sensor_smoke, cfg.binary_sensor_battery_low, cfg.binary_sensor_tamper],
|
||||
'902010/24': [cfg.binary_sensor_smoke, cfg.binary_sensor_battery_low, cfg.binary_sensor_tamper],
|
||||
'ROB_200-014-0': [cfg.light_brightness],
|
||||
'4090631P7': [cfg.light_brightness_colortemp_colorxy],
|
||||
'SGMHM-I1': [cfg.binary_sensor_gas, cfg.binary_sensor_battery_low, cfg.binary_sensor_tamper],
|
||||
'STHM-I1H': [cfg.sensor_humidity, cfg.sensor_temperature, cfg.sensor_battery],
|
||||
'BDHM8E27W70-I1': [cfg.light_brightness_colortemp],
|
||||
'M420': [cfg.sensor_battery],
|
||||
'8718696167991': [cfg.light_brightness_colortemp_colorxy],
|
||||
'GP-LBU019BBAWU': [cfg.light_brightness],
|
||||
'371000001': [cfg.light_brightness_colortemp],
|
||||
'10011725': [cfg.light_brightness_colortemp_colorxy],
|
||||
'929002277501': [cfg.light_brightness],
|
||||
'RS 230 C': [cfg.light_brightness_colortemp_colorxy],
|
||||
'LED1903C5': [cfg.light_brightness_colortemp],
|
||||
'1402755': [cfg.light_brightness],
|
||||
'4503848C5': [cfg.light_brightness_colortemp],
|
||||
'500.48': [cfg.light_brightness],
|
||||
'TS0042': [cfg.sensor_action],
|
||||
'RHK06': [cfg.binary_sensor_contact, cfg.sensor_battery],
|
||||
'RHK07': [cfg.sensor_action, cfg.sensor_battery],
|
||||
'RHK08': [cfg.sensor_temperature, cfg.sensor_humidity, cfg.sensor_battery],
|
||||
'RHK09': [cfg.binary_sensor_occupancy, cfg.sensor_battery],
|
||||
'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],
|
||||
'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],
|
||||
};
|
||||
|
||||
Object.keys(mapping).forEach((key) => {
|
||||
@@ -977,8 +1265,8 @@ Object.keys(mapping).forEach((key) => {
|
||||
* This extensions handles integration with HomeAssistant
|
||||
*/
|
||||
class HomeAssistant extends BaseExtension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
super(zigbee, mqtt, state, publishEntityState);
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
|
||||
// A map of all discoverd devices
|
||||
this.discovered = {};
|
||||
@@ -993,6 +1281,19 @@ class HomeAssistant extends BaseExtension {
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
onDeviceRemoved(device) {
|
||||
const mappedModel = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
|
||||
if (mappedModel) {
|
||||
logger.info(`Clearing Home Assistant discovery topic for '${device.ieeeAddr}'`);
|
||||
this.getConfigs(mappedModel).forEach((config) => {
|
||||
const topic = this.getDiscoveryTopic(config, device);
|
||||
this.mqtt.publish(topic, null, {retain: true, qos: 0}, this.discoveryTopic);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async onMQTTConnected() {
|
||||
@@ -1002,26 +1303,34 @@ class HomeAssistant extends BaseExtension {
|
||||
for (const device of this.zigbee.getClients()) {
|
||||
const mappedModel = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
|
||||
if (mappedModel) {
|
||||
this.discover(device.ieeeAddr, mappedModel, true);
|
||||
this.discover(device, mappedModel, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
discover(entityID, mappedModel, force=false) {
|
||||
getConfigs(mappedModel) {
|
||||
const configs = mapping[mappedModel.model].slice();
|
||||
if (mappedModel.hasOwnProperty('ota')) {
|
||||
configs.push(cfg.binary_sensor_update_available);
|
||||
}
|
||||
return configs;
|
||||
}
|
||||
|
||||
discover(device, mappedModel, force=false) {
|
||||
// Check if already discoverd and check if there are configs.
|
||||
const discover = force || !this.discovered[entityID];
|
||||
const discover = force || !this.discovered[device.ieeeAddr];
|
||||
if (!discover) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entity = settings.getEntity(entityID);
|
||||
const entity = settings.getEntity(device.ieeeAddr);
|
||||
if (!entity || (entity.type === 'device' && !mapping[mappedModel.model]) ||
|
||||
(entity.hasOwnProperty('homeassistant') && !entity.homeassistant)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mapping[mappedModel.model].forEach((config) => {
|
||||
const topic = `${config.type}/${entity.ID}/${config.object_id}/config`;
|
||||
this.getConfigs(mappedModel).forEach((config) => {
|
||||
const topic = this.getDiscoveryTopic(config, device);
|
||||
const payload = {...config.discovery_payload};
|
||||
const stateTopic = `${settings.get().mqtt.base_topic}/${entity.friendlyName}`;
|
||||
|
||||
@@ -1042,11 +1351,7 @@ class HomeAssistant extends BaseExtension {
|
||||
payload.tilt_status_topic = stateTopic;
|
||||
}
|
||||
|
||||
// Set json_attributes_topic for types which support this
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/840
|
||||
if (['binary_sensor', 'sensor', 'lock', 'climate', 'fan'].includes(config.type)) {
|
||||
payload.json_attributes_topic = payload.state_topic;
|
||||
}
|
||||
payload.json_attributes_topic = stateTopic;
|
||||
|
||||
// Set (unique) name
|
||||
payload.name = `${entity.friendlyName}_${config.object_id}`;
|
||||
@@ -1135,6 +1440,10 @@ class HomeAssistant extends BaseExtension {
|
||||
payload.speed_command_topic = `${stateTopic}/set/fan_mode`;
|
||||
}
|
||||
|
||||
if (payload.action_topic) {
|
||||
payload.action_topic = stateTopic;
|
||||
}
|
||||
|
||||
// Override configuration with user settings.
|
||||
if (entity.hasOwnProperty('homeassistant')) {
|
||||
const add = (obj) => {
|
||||
@@ -1161,7 +1470,7 @@ class HomeAssistant extends BaseExtension {
|
||||
this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0}, this.discoveryTopic);
|
||||
});
|
||||
|
||||
this.discovered[entityID] = true;
|
||||
this.discovered[device.ieeeAddr] = true;
|
||||
}
|
||||
|
||||
onMQTTMessage(topic, message) {
|
||||
@@ -1186,10 +1495,14 @@ class HomeAssistant extends BaseExtension {
|
||||
onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
|
||||
const device = data.device;
|
||||
if (device && mappedDevice) {
|
||||
this.discover(device.ieeeAddr, mappedDevice);
|
||||
this.discover(device, mappedDevice);
|
||||
}
|
||||
}
|
||||
|
||||
getDiscoveryTopic(config, device) {
|
||||
return `${config.type}/${device.ieeeAddr}/${config.object_id}/config`;
|
||||
}
|
||||
|
||||
// Only for homeassistant.test.js
|
||||
_getMapping() {
|
||||
return mapping;
|
||||
|
||||
@@ -5,8 +5,8 @@ const logger = require('../util/logger');
|
||||
const BaseExtension = require('./baseExtension');
|
||||
|
||||
class NetworkMap extends BaseExtension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
super(zigbee, mqtt, state, publishEntityState);
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
|
||||
// Subscribe to topic.
|
||||
this.topic = `${settings.get().mqtt.base_topic}/bridge/networkmap`;
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
const settings = require('../util/settings');
|
||||
const logger = require('../util/logger');
|
||||
const assert = require('assert');
|
||||
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/ota_update/.+$`);
|
||||
const BaseExtension = require('./baseExtension');
|
||||
const MINUTES_10 = 1000 * 60 * 10;
|
||||
|
||||
class OTAUpdate extends BaseExtension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
this.inProgress = new Set();
|
||||
this.lastChecked = {};
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/ota_update/check`);
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/ota_update/update`);
|
||||
}
|
||||
|
||||
async onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
|
||||
if (data.type !== 'commandQueryNextImageRequest' || !mappedDevice.hasOwnProperty('ota')) return;
|
||||
|
||||
// When a device does a next image request, it will usually do it a few times after each other
|
||||
// with only 10 - 60 seconds inbetween. It doesn' make sense to check for a new update
|
||||
// each time.
|
||||
const check = this.lastChecked.hasOwnProperty(data.device.ieeeAddr) ?
|
||||
(Date.now() - this.lastChecked[data.device.ieeeAddr]) > MINUTES_10 : true;
|
||||
if (!check || this.inProgress.has(data.device.ieeeAddr)) return;
|
||||
|
||||
this.lastChecked[data.device.ieeeAddr] = Date.now();
|
||||
const available = await mappedDevice.ota.isUpdateAvailable(data.device, logger, data.data);
|
||||
this.publishEntityState(data.device.ieeeAddr, {update_available: available});
|
||||
|
||||
if (available) {
|
||||
const message = `Update available for '${settingsDevice.friendly_name}'`;
|
||||
logger.info(message);
|
||||
this.mqtt.log('ota_update', message, {status: 'available', device: settingsDevice.friendly_name});
|
||||
}
|
||||
}
|
||||
|
||||
async readSoftwareBuildIDAndDateCode(device, update) {
|
||||
try {
|
||||
const endpoint = device.endpoints.find((e) => e.supportsInputCluster('genBasic'));
|
||||
const result = await endpoint.read('genBasic', ['dateCode', 'swBuildId']);
|
||||
|
||||
if (update) {
|
||||
device.softwareBuildID = result.swBuildId;
|
||||
device.dateCode = result.dateCode;
|
||||
device.save();
|
||||
}
|
||||
|
||||
return {softwareBuildID: result.swBuildId, dateCode: result.dateCode};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async onMQTTMessage(topic, message) {
|
||||
if (!topic.match(topicRegex)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const device = this.zigbee.resolveEntity(message);
|
||||
assert(device != null && device.type === 'device', 'Device not found or not a device');
|
||||
if (!device.mapped || !device.mapped.ota) {
|
||||
const message = `Device '${device.name}' does not support OTA updates`;
|
||||
logger.error(message);
|
||||
this.mqtt.log('ota_update', message, {status: `not_supported`, device: device.name});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.inProgress.has(device.device.ieeeAddr)) {
|
||||
logger.error(`Update or check already in progress for '${device.name}', skipping...`);
|
||||
return;
|
||||
}
|
||||
this.inProgress.add(device.device.ieeeAddr);
|
||||
|
||||
const type = topic.split('/')[3];
|
||||
if (type === 'check') {
|
||||
const message = `Checking if update available for '${device.name}'`;
|
||||
logger.info(message);
|
||||
this.mqtt.log('ota_update', message, {status: `checking_if_available`, device: device.name});
|
||||
try {
|
||||
const available = await device.mapped.ota.isUpdateAvailable(device.device, logger);
|
||||
const message=(available ?
|
||||
`Update available for '${device.name}'` : `No update available for '${device.name}'`);
|
||||
logger.info(message);
|
||||
const meta = {status: available ? 'available' : 'not_available', device: device.name};
|
||||
this.mqtt.log('ota_update', message, meta);
|
||||
this.publishEntityState(device.device.ieeeAddr, {update_available: available});
|
||||
this.lastChecked[device.device.ieeeAddr] = Date.now();
|
||||
} catch (error) {
|
||||
const message = `Failed to check if update available for '${device.name}' (${error.message})`;
|
||||
logger.error(message);
|
||||
this.mqtt.log('ota_update', message, {status: `check_failed`, device: device.name});
|
||||
}
|
||||
} else { // type === 'update'
|
||||
const message = `Updating '${device.name}' to latest firmware`;
|
||||
logger.info(message);
|
||||
this.mqtt.log('ota_update', message, {status: `update_in_progress`, device: device.name});
|
||||
try {
|
||||
const onProgress = (progress, remaining) => {
|
||||
let message = `Update of '${device.name}' at ${progress}%`;
|
||||
if (remaining) {
|
||||
message += `, +- ${Math.round(remaining / 60)} minutes remaining`;
|
||||
}
|
||||
|
||||
logger.info(message);
|
||||
this.mqtt.log('ota_update', message, {status: `update_progress`, device: device.name, progress});
|
||||
};
|
||||
|
||||
const from_ = await this.readSoftwareBuildIDAndDateCode(device.device, false);
|
||||
await device.mapped.ota.updateToLatest(device.device, logger, onProgress);
|
||||
const to = await this.readSoftwareBuildIDAndDateCode(device.device, true);
|
||||
const [fromS, toS] = [JSON.stringify(from_), JSON.stringify(to)];
|
||||
const message = `Finished update of '${device.name}'` + (to ? `, from '${fromS}' to '${toS}'` : ``);
|
||||
logger.info(message);
|
||||
const meta = {status: `update_succeeded`, device: device.name, from: from_, to};
|
||||
this.mqtt.log('ota_update', message, meta);
|
||||
this.publishEntityState(device.device.ieeeAddr, {update_available: false});
|
||||
} catch (error) {
|
||||
const message = `Update of '${device.name}' failed (${error.message})`;
|
||||
logger.error(message);
|
||||
this.mqtt.log('ota_update', message, {status: `update_failed`, device: device.name});
|
||||
}
|
||||
}
|
||||
|
||||
this.inProgress.delete(device.device.ieeeAddr);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OTAUpdate;
|
||||
@@ -9,8 +9,8 @@ const BaseExtension = require('./baseExtension');
|
||||
* This extensions soft resets the ZNP after a certain timeout.
|
||||
*/
|
||||
class SoftReset extends BaseExtension {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
super(zigbee, mqtt, state, publishEntityState);
|
||||
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
||||
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
||||
this.timer = null;
|
||||
this.timeout = utils.secondsToMilliseconds(settings.get().advanced.soft_reset_timeout);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,11 @@ class MQTT extends events.EventEmitter {
|
||||
},
|
||||
};
|
||||
|
||||
if (mqttSettings.keepalive) {
|
||||
logger.debug(`Using MQTT keepalive: ${mqttSettings.keepalive}`);
|
||||
options.keepalive = mqttSettings.keepalive;
|
||||
}
|
||||
|
||||
if (mqttSettings.ca) {
|
||||
logger.debug(`MQTT SSL/TLS: Path to CA certificate = ${mqttSettings.ca}`);
|
||||
options.ca = fs.readFileSync(mqttSettings.ca);
|
||||
|
||||
+57
-32
@@ -6,16 +6,25 @@ const fs = require('fs');
|
||||
const fx = require('mkdir-recursive');
|
||||
const rimraf = require('rimraf');
|
||||
const colorizer = winston.format.colorize();
|
||||
const assert = require('assert');
|
||||
|
||||
// Directoy to log to
|
||||
// What transports to enable
|
||||
const output = settings.get().advanced.log_output;
|
||||
|
||||
// Directory to log to
|
||||
const timestamp = moment(Date.now()).format('YYYY-MM-DD.HH-mm-ss');
|
||||
const directory = settings.get().advanced.log_directory.replace('%TIMESTAMP%', timestamp);
|
||||
const logFilename = settings.get().advanced.log_file.replace('%TIMESTAMP%', timestamp);
|
||||
|
||||
// Make sure that log directoy exsists
|
||||
fx.mkdirSync(directory);
|
||||
// Make sure that log directoy exsists when not logging to stdout only
|
||||
if (output.includes('file')) {
|
||||
fx.mkdirSync(directory);
|
||||
}
|
||||
|
||||
// Determine the log level.
|
||||
const level = settings.get().advanced.log_level;
|
||||
const validLevels = ['info', 'error', 'warn', 'debug'];
|
||||
assert(validLevels.includes(level), `'${level}' is not a valid log_level, use one of '${validLevels.join(', ')}'`);
|
||||
|
||||
const levelWithCompensatedLength = {
|
||||
'info': 'info ',
|
||||
@@ -24,26 +33,16 @@ const levelWithCompensatedLength = {
|
||||
'debug': 'debug',
|
||||
};
|
||||
|
||||
// Create logger
|
||||
const transports = {
|
||||
file: new winston.transports.File({
|
||||
filename: path.join(directory, 'log.txt'),
|
||||
json: false,
|
||||
/* istanbul ignore next */
|
||||
const timestampFormat = () => moment().format(settings.get().advanced.timestamp_format);
|
||||
|
||||
// Setup default console logger
|
||||
const transportsToUse = [
|
||||
new winston.transports.Console({
|
||||
level,
|
||||
maxFiles: 3, // Keep last 3 files
|
||||
maxsize: 10000000, // 10MB
|
||||
silent: !output.includes('console'),
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.printf(/* istanbul ignore next */(info) => {
|
||||
const {timestamp, level, message} = info;
|
||||
return `${levelWithCompensatedLength[level]} ${timestamp.split('.')[0]}: ${message}`;
|
||||
}),
|
||||
),
|
||||
}),
|
||||
console: new winston.transports.Console({
|
||||
level,
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.timestamp({format: timestampFormat}),
|
||||
winston.format.printf(/* istanbul ignore next */(info) => {
|
||||
const {timestamp, level, message} = info;
|
||||
const prefix = colorizer.colorize(level, `zigbee2mqtt:${levelWithCompensatedLength[level]}`);
|
||||
@@ -51,14 +50,30 @@ const transports = {
|
||||
}),
|
||||
),
|
||||
}),
|
||||
};
|
||||
];
|
||||
|
||||
const logger = winston.createLogger({
|
||||
transports: [
|
||||
transports.console,
|
||||
transports.file,
|
||||
],
|
||||
});
|
||||
// Add file logger when enabled
|
||||
// NOTE: the initiation of the logger, even when not added as transport tries to create the logging directory
|
||||
if (output.includes('file')) {
|
||||
transportsToUse.push(new winston.transports.File({
|
||||
filename: path.join(directory, logFilename),
|
||||
json: false,
|
||||
level,
|
||||
tailable: true,
|
||||
maxFiles: 3, // Keep last 3 files
|
||||
maxsize: 10000000, // 10MB
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({format: timestampFormat}),
|
||||
winston.format.printf(/* istanbul ignore next */(info) => {
|
||||
const {timestamp, level, message} = info;
|
||||
return `${levelWithCompensatedLength[level]} ${timestamp.split('.')[0]}: ${message}`;
|
||||
}),
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
// Create logger
|
||||
const logger = winston.createLogger({transports: transportsToUse});
|
||||
|
||||
// Cleanup any old log directory.
|
||||
function cleanup() {
|
||||
@@ -80,11 +95,21 @@ function cleanup() {
|
||||
}
|
||||
|
||||
logger.cleanup = cleanup;
|
||||
logger.directory = directory;
|
||||
logger.getLevel = () => transports.console.level;
|
||||
logger.getLevel = () => transportsToUse[0].level;
|
||||
logger.setLevel = (level) => {
|
||||
transports.console.level = level;
|
||||
transports.file.level = level;
|
||||
transportsToUse.forEach((transport) => transport.level = level);
|
||||
};
|
||||
|
||||
// Print to user what logging is enabled
|
||||
if (output.includes('file')) {
|
||||
if (output.includes('console')) {
|
||||
logger.info(`Logging to console and directory: '${directory}' filename: ${logFilename}`);
|
||||
} else {
|
||||
logger.info(`Logging to directory: '${directory}' filename: ${logFilename}`);
|
||||
}
|
||||
logger.cleanup();
|
||||
} else if (output.includes('console')) {
|
||||
logger.info(`Logging to console only'`);
|
||||
}
|
||||
|
||||
module.exports = logger;
|
||||
|
||||
+33
-4
@@ -1,5 +1,6 @@
|
||||
const data = require('./data');
|
||||
const file = data.joinPath('configuration.yaml');
|
||||
const utils = require('./utils');
|
||||
const file = process.env.ZIGBEE2MQTT_CONFIG || data.joinPath('configuration.yaml');
|
||||
const objectAssignDeep = require(`object-assign-deep`);
|
||||
const path = require('path');
|
||||
const yaml = require('./yaml');
|
||||
@@ -38,11 +39,13 @@ const defaults = {
|
||||
},
|
||||
},
|
||||
experimental: {
|
||||
// json or attribute
|
||||
// json or attribute or attribute_and_json
|
||||
output: 'json',
|
||||
},
|
||||
advanced: {
|
||||
log_output: ['console', 'file'],
|
||||
log_directory: path.join(data.getPath(), 'log', '%TIMESTAMP%'),
|
||||
log_file: 'log.txt',
|
||||
log_level: /* istanbul ignore next */ process.env.DEBUG ? 'debug' : 'info',
|
||||
soft_reset_timeout: 0,
|
||||
pan_id: 0x1a62,
|
||||
@@ -54,6 +57,7 @@ const defaults = {
|
||||
// Availability timeout in seconds, disabled by default.
|
||||
availability_timeout: 0,
|
||||
availability_blacklist: [],
|
||||
availability_whitelist: [],
|
||||
|
||||
/**
|
||||
* Home Assistant requires ALL attributes to be present in ALL MQTT messages send by the device.
|
||||
@@ -99,6 +103,12 @@ const defaults = {
|
||||
* Home Assistant status topic
|
||||
*/
|
||||
homeassistant_status_topic: 'hass/status',
|
||||
|
||||
/**
|
||||
* Configurable timestampFormat
|
||||
* https://github.com/Koenkk/zigbee2mqtt/commit/44db557a0c83f419d66755d14e460cd78bd6204e
|
||||
*/
|
||||
timestamp_format: 'YYYY-MM-DD HH:mm:ss',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -112,6 +122,7 @@ const schema = {
|
||||
properties: {
|
||||
base_topic: {type: 'string'},
|
||||
server: {type: 'string'},
|
||||
keepalive: {type: 'number'},
|
||||
ca: {type: 'string'},
|
||||
key: {type: 'string'},
|
||||
cert: {type: 'string'},
|
||||
@@ -132,6 +143,12 @@ const schema = {
|
||||
},
|
||||
ban: {type: 'array', items: {type: 'string'}},
|
||||
whitelist: {type: 'array', items: {type: 'string'}},
|
||||
experimental: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
transmit_power: {type: 'number'},
|
||||
},
|
||||
},
|
||||
advanced: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -140,7 +157,9 @@ const schema = {
|
||||
channel: {type: 'number', minimum: 11, maximum: 26},
|
||||
cache_state: {type: 'boolean'},
|
||||
log_level: {type: 'string', enum: ['info', 'warn', 'error', 'debug']},
|
||||
log_output: {type: 'array', items: {type: 'string'}},
|
||||
log_directory: {type: 'string'},
|
||||
log_file: {type: 'string'},
|
||||
baudrate: {type: 'number'},
|
||||
rtscts: {type: 'boolean'},
|
||||
soft_reset_timeout: {type: 'number', minimum: 0},
|
||||
@@ -149,9 +168,11 @@ const schema = {
|
||||
elapsed: {type: 'boolean'},
|
||||
availability_timeout: {type: 'number', minimum: 0},
|
||||
availability_blacklist: {type: 'array', items: {type: 'string'}},
|
||||
availability_whitelist: {type: 'array', items: {type: 'string'}},
|
||||
report: {type: 'boolean'},
|
||||
homeassistant_discovery_topic: {type: 'string'},
|
||||
homeassistant_status_topic: {type: 'string'},
|
||||
timestamp_format: {type: 'string'},
|
||||
},
|
||||
},
|
||||
map_options: {
|
||||
@@ -270,11 +291,13 @@ function write() {
|
||||
function validate() {
|
||||
const validate = ajv.compile(schema);
|
||||
const valid = validate(_settings);
|
||||
const postfixes = utils.getPostfixes();
|
||||
|
||||
// Verify that all friendly names are unique
|
||||
const names = [];
|
||||
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}'`);
|
||||
names.push(name);
|
||||
};
|
||||
Object.values(_settingsWithDefaults.devices).forEach((d) => check(d.friendly_name));
|
||||
@@ -577,9 +600,15 @@ function changeFriendlyName(IDorName, newName) {
|
||||
throw new Error(`friendly_name '${newName}' is already in use`);
|
||||
}
|
||||
|
||||
const device = getDeviceThrowIfNotExists(IDorName);
|
||||
const settings = get();
|
||||
settings.devices[device.ID].friendly_name = newName;
|
||||
if (getDevice(IDorName)) {
|
||||
settings.devices[getDevice(IDorName).ID].friendly_name = newName;
|
||||
} else if (getGroup(IDorName)) {
|
||||
settings.groups[getGroup(IDorName).ID].friendly_name = newName;
|
||||
} else {
|
||||
throw new Error(`Device or group '${IDorName}' does not exist`);
|
||||
}
|
||||
|
||||
write();
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -28,9 +28,9 @@ function toLocalISOString(dDate) {
|
||||
}
|
||||
|
||||
const postfixes = [
|
||||
'left', 'right', 'center', 'bottom_left', 'bottom_right', 'l1', 'l2', 'default',
|
||||
'left', 'right', 'center', 'bottom_left', 'bottom_right', 'l1', 'l2', 'l3', 'l4', 'default',
|
||||
'top_left', 'top_right', 'white', 'rgb', 'system', 'top', 'bottom', 'center_left', 'center_right',
|
||||
'ep1', 'ep2',
|
||||
'ep1', 'ep2', 'row_1', 'row_2', 'row_3', 'row_4', 'relay',
|
||||
];
|
||||
|
||||
function flatten(arr) {
|
||||
|
||||
+13
-2
@@ -9,7 +9,7 @@ const objectAssignDeep = require('object-assign-deep');
|
||||
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
|
||||
const postfixes = utils.getPostfixes();
|
||||
const keyEndpointByNumber = new RegExp(`.*/([0-9].*)$`);
|
||||
const keyEndpointByNumber = new RegExp(`.*/([0-9]*)$`);
|
||||
|
||||
const herdsmanSettings = {
|
||||
network: {
|
||||
@@ -75,7 +75,14 @@ class Zigbee extends events.EventEmitter {
|
||||
|
||||
// Check if we have to turn off the led
|
||||
if (settings.get().serial.disable_led) {
|
||||
this.herdsman.setLED(false);
|
||||
await this.herdsman.setLED(false);
|
||||
}
|
||||
|
||||
// Check if we have to set a transmit power
|
||||
if (settings.get().experimental.hasOwnProperty('transmit_power')) {
|
||||
const transmitPower = settings.get().experimental.transmit_power;
|
||||
await this.herdsman.setTransmitPower(transmitPower);
|
||||
logger.info(`Set transmit power to '${transmitPower}'`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,6 +244,10 @@ class Zigbee extends events.EventEmitter {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async touchlinkFactoryReset() {
|
||||
return this.herdsman.touchlinkFactoryReset();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Zigbee;
|
||||
|
||||
Generated
+11361
-3669
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zigbee2mqtt",
|
||||
"version": "1.7.0",
|
||||
"version": "1.11.0",
|
||||
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
@@ -8,7 +8,7 @@
|
||||
"url": "git+https://github.com/Koenkk/zigbee2mqtt.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": "10"
|
||||
"node": "^10 || ^12 || ^13"
|
||||
},
|
||||
"keywords": [
|
||||
"xiaomi",
|
||||
@@ -45,8 +45,8 @@
|
||||
"rimraf": "*",
|
||||
"semver": "*",
|
||||
"winston": "*",
|
||||
"zigbee-herdsman": "0.12.3",
|
||||
"zigbee-herdsman-converters": "11.1.40"
|
||||
"zigbee-herdsman": "0.12.55",
|
||||
"zigbee-herdsman-converters": "12.0.32"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "*",
|
||||
|
||||
+2
-2
@@ -26,7 +26,7 @@ chown -R pi:pi /opt/zigbee2mqtt
|
||||
|
||||
echo "Running install. This might take a while and can produce som expected errors"
|
||||
cd /opt/zigbee2mqtt || exit
|
||||
su pi -c "npm install"
|
||||
su pi -c "npm ci"
|
||||
|
||||
echo "Creating service file zigbee2mqtt.service"
|
||||
service_path="/etc/systemd/system/zigbee2mqtt.service"
|
||||
@@ -70,4 +70,4 @@ fi
|
||||
return 0
|
||||
}
|
||||
|
||||
[[ "$_" == "$0" ]] && zigbee2mqtt-install-package
|
||||
[[ "$_" == "$0" ]] && zigbee2mqtt-install-package
|
||||
|
||||
@@ -22,6 +22,8 @@ describe('Bridge config', () => {
|
||||
data.writeDefaultConfiguration();
|
||||
settings._reRead();
|
||||
data.writeDefaultState();
|
||||
logger.info.mockClear();
|
||||
logger.warn.mockClear();
|
||||
});
|
||||
|
||||
it('Should publish bridge configuration on startup', async () => {
|
||||
@@ -160,7 +162,7 @@ describe('Bridge config', () => {
|
||||
const payload = JSON.parse(MQTT.publish.mock.calls[0][1]);
|
||||
expect(payload.length).toStrictEqual(Object.values(zigbeeHerdsman.devices).length);
|
||||
expect(payload[0]).toStrictEqual({"ieeeAddr": "0x00124b00120144ae", "type": "Coordinator", "dateCode": "20190425", "friendly_name": "Coordinator", networkAddress: 0, softwareBuildID: "z-Stack", lastSeen: 100});
|
||||
expect(payload[1]).toStrictEqual({"friendly_name": "bulb", "ieeeAddr": "0x000b57fffec6a5b2", "lastSeen": 1000, "manufacturerID": 4476, "model": "LED1545G12", "modelID": "TRADFRI bulb E27 WS opal 980lm", "networkAddress": 40369, "powerSource": "Mains (single phase)", "type": "Router"});
|
||||
expect(payload[1]).toStrictEqual({"dateCode": null, "friendly_name": "bulb", "ieeeAddr": "0x000b57fffec6a5b2", "lastSeen": 1000, "manufacturerID": 4476, "model": "LED1545G12", "modelID": "TRADFRI bulb E27 WS opal 980lm", "networkAddress": 40369, "powerSource": "Mains (single phase)", "type": "Router"});
|
||||
Date.now = now;
|
||||
});
|
||||
|
||||
@@ -201,6 +203,47 @@ describe('Bridge config', () => {
|
||||
expect(settings.getDevice('bulb_color2')).toStrictEqual(bulb_color2);
|
||||
});
|
||||
|
||||
it('Should allow rename groups', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
expect(settings.getGroup(1)).toStrictEqual({"ID": 1, devices: [], friendlyName: "group_1", "friendly_name": "group_1", optimistic: true, retain: false});
|
||||
MQTT.events.message('zigbee2mqtt/bridge/config/rename', JSON.stringify({old: 'group_1', new: 'group_1_renamed'}));
|
||||
await flushPromises();
|
||||
expect(settings.getGroup(1)).toStrictEqual({"ID": 1, devices: [], friendlyName: "group_1_renamed", "friendly_name": "group_1_renamed", optimistic: true, retain: false});
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/log',
|
||||
JSON.stringify({type: 'group_renamed', message: {from: 'group_1', to: 'group_1_renamed'}}),
|
||||
{qos: 0, retain: false},
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Should allow to rename last joined device', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const payload = {device};
|
||||
await zigbeeHerdsman.events.deviceJoined(payload);
|
||||
await flushPromises();
|
||||
expect(settings.getDevice('0x000b57fffec6a5b2').friendlyName).toStrictEqual('bulb');
|
||||
MQTT.events.message('zigbee2mqtt/bridge/config/rename_last', 'bulb_new_name');
|
||||
await flushPromises();
|
||||
expect(settings.getDevice('0x000b57fffec6a5b2').friendlyName).toStrictEqual('bulb_new_name');
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/bridge/log',
|
||||
JSON.stringify({type: 'device_renamed', message: {from: 'bulb', to: 'bulb_new_name'}}),
|
||||
{qos: 0, retain: false},
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Shouldnt rename when no device has been joined', async () => {
|
||||
controller = new Controller();
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
expect(settings.getDevice('0x000b57fffec6a5b2').friendlyName).toStrictEqual('bulb');
|
||||
MQTT.events.message('zigbee2mqtt/bridge/config/rename_last', 'bulb_new_name');
|
||||
await flushPromises();
|
||||
expect(settings.getDevice('0x000b57fffec6a5b2').friendlyName).toStrictEqual('bulb');
|
||||
});
|
||||
|
||||
it('Should allow to add groups', async () => {
|
||||
zigbeeHerdsman.createGroup.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/config/add_group', 'new_group');
|
||||
@@ -238,10 +281,11 @@ describe('Bridge config', () => {
|
||||
});
|
||||
|
||||
it('Should allow to remove groups', async () => {
|
||||
settings.addGroup('to_be_removed')
|
||||
MQTT.events.message('zigbee2mqtt/bridge/config/remove_group', 'to_be_removed');
|
||||
const group = zigbeeHerdsman.groups.group_1;
|
||||
MQTT.events.message('zigbee2mqtt/bridge/config/remove_group', 'group_1');
|
||||
await flushPromises();
|
||||
expect(settings.getGroup('to_be_removed')).toStrictEqual(null);
|
||||
expect(group.removeFromDatabase).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Shouldnt allow add groups without id or friendly_name in json', async () => {
|
||||
@@ -339,8 +383,9 @@ describe('Bridge config', () => {
|
||||
MQTT.events.message('zigbee2mqtt/bridge/config/remove', 'bulb_color');
|
||||
await flushPromises();
|
||||
expect(device.removeFromNetwork).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(settings.getDevice('bulb_color')).toStrictEqual({"ID": "0x000b57fffec6a5b3", "friendlyName": "bulb_color", "friendly_name": "bulb_color", "retain": false})
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should handle when ban fails', async () => {
|
||||
@@ -352,7 +397,28 @@ describe('Bridge config', () => {
|
||||
MQTT.events.message('zigbee2mqtt/bridge/config/ban', 'bulb_color');
|
||||
await flushPromises();
|
||||
expect(device.removeFromNetwork).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(settings.getDevice('bulb_color')).toStrictEqual({"ID": "0x000b57fffec6a5b3", "friendlyName": "bulb_color", "friendly_name": "bulb_color", "retain": false})
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should allow to touchlink factory reset (OK)', async () => {
|
||||
zigbeeHerdsman.touchlinkFactoryReset.mockClear();
|
||||
|
||||
zigbeeHerdsman.touchlinkFactoryReset.mockReturnValueOnce(true);
|
||||
MQTT.events.message('zigbee2mqtt/bridge/config/touchlink/factory_reset', '');
|
||||
await flushPromises();
|
||||
expect(zigbeeHerdsman.touchlinkFactoryReset).toHaveBeenCalledTimes(1);
|
||||
expect(logger.info).toHaveBeenCalledWith('Successfully factory reset device through Touchlink');
|
||||
});
|
||||
|
||||
it('Should allow to touchlink factory reset (FAILS)', async () => {
|
||||
zigbeeHerdsman.touchlinkFactoryReset.mockClear();
|
||||
|
||||
zigbeeHerdsman.touchlinkFactoryReset.mockReturnValueOnce(false);
|
||||
MQTT.events.message('zigbee2mqtt/bridge/config/touchlink/factory_reset', '');
|
||||
await flushPromises();
|
||||
expect(zigbeeHerdsman.touchlinkFactoryReset).toHaveBeenCalledTimes(1);
|
||||
expect(logger.warn).toHaveBeenCalledWith('Failed to factory reset device through Touchlink');
|
||||
});
|
||||
});
|
||||
|
||||
+30
-2
@@ -30,10 +30,10 @@ describe('Controller', () => {
|
||||
|
||||
it('Start controller', async () => {
|
||||
await controller.start();
|
||||
expect(logger.cleanup).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsman.constructor).toHaveBeenCalledWith({"network":{"panID":6754,"extendedPanID":[221,221,221,221,221,221,221,221],"channelList":[11],"networkKey":[1,3,5,7,9,11,13,15,0,2,4,6,8,10,12,13]},"databasePath":path.join(data.mockDir, "database.db"), "databaseBackupPath":path.join(data.mockDir, "database.db.backup"),"backupPath":path.join(data.mockDir, "coordinator_backup.json"),"acceptJoiningDeviceHandler": expect.any(Function),"serialPort":{"baudRate":115200,"rtscts":true,"path":"/dev/dummy"}});
|
||||
expect(zigbeeHerdsman.start).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsman.setLED).toHaveBeenCalledTimes(0);
|
||||
expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledTimes(0);
|
||||
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true);
|
||||
expect(logger.info).toHaveBeenCalledWith(`Currently ${Object.values(zigbeeHerdsman.devices).length - 1} devices are joined:`)
|
||||
@@ -57,6 +57,7 @@ describe('Controller', () => {
|
||||
const configuration = {
|
||||
base_topic: "zigbee2mqtt",
|
||||
server: "mqtt://localhost",
|
||||
keepalive: 30,
|
||||
ca, cert, key,
|
||||
password: 'pass',
|
||||
user: 'user1',
|
||||
@@ -69,6 +70,7 @@ describe('Controller', () => {
|
||||
expect(MQTT.connect).toHaveBeenCalledTimes(1);
|
||||
const expected = {
|
||||
"will": {"payload": "offline", "retain": true, "topic": "zigbee2mqtt/bridge/state"},
|
||||
keepalive: 30,
|
||||
ca: Buffer.from([99, 97]),
|
||||
key: Buffer.from([107, 101, 121]),
|
||||
cert: Buffer.from([99, 101, 114, 116]),
|
||||
@@ -176,6 +178,13 @@ describe('Controller', () => {
|
||||
expect(zigbeeHerdsman.setLED).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('Start controller with transmit power', async () => {
|
||||
settings.set(['experimental', 'transmit_power'], 14);
|
||||
await controller.start();
|
||||
expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledTimes(1);
|
||||
expect(zigbeeHerdsman.setTransmitPower).toHaveBeenCalledWith(14);
|
||||
});
|
||||
|
||||
it('Start controller and stop', async () => {
|
||||
await controller.start();
|
||||
await controller.stop();
|
||||
@@ -390,13 +399,32 @@ describe('Controller', () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/dummy-2", 'no', {"qos": 0, "retain": true}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Publish entity state attribute_json output', async () => {
|
||||
await controller.start();
|
||||
settings.set(['experimental', 'output'], 'attribute_and_json');
|
||||
MQTT.publish.mockClear();
|
||||
await controller.publishEntityState('bulb', {state: 'ON', brightness: 200, color_temp: 370, linkquality: 99});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(5);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/brightness", "200", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/color_temp", "370", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/linkquality", "99", {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", '{"state":"ON","brightness":200,"color_temp":370,"linkquality":99}', {"qos": 0, "retain": true}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Publish entity state with device information', async () => {
|
||||
await controller.start();
|
||||
settings.set(['mqtt', 'include_device_information'], true);
|
||||
MQTT.publish.mockClear();
|
||||
await controller.publishEntityState('bulb', {state: 'ON'});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', '{"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"device":{"friendlyName":"bulb","ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369,"type":"Router","manufacturerID":4476,"powerSource":"Mains (single phase)"}}', {"qos": 0, "retain": true}, expect.any(Function));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', '{"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"device":{"friendlyName":"bulb","model":"LED1545G12","ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369,"type":"Router","manufacturerID":4476,"powerSource":"Mains (single phase)","dateCode":null}}', {"qos": 0, "retain": true}, expect.any(Function));
|
||||
|
||||
// Unsupported device should have model "unknown"
|
||||
await controller.publishEntityState('unsupported2', {state: 'ON'});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/unsupported2', '{"state":"ON","device":{"friendlyName":"unsupported2","model":"unknown","ieeeAddr":"0x0017880104e45529","networkAddress":6536,"type":"EndDevice","manufacturerID":0,"powerSource":"Battery","dateCode":null}}', {"qos": 0, "retain": false}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Publish entity state no empty messages', async () => {
|
||||
|
||||
+134
-14
@@ -4,6 +4,8 @@ const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
|
||||
zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b3');
|
||||
zigbeeHerdsman.returnDevices.push('0x00124b00120144ae');
|
||||
zigbeeHerdsman.returnDevices.push('0x0017880104e45553');
|
||||
zigbeeHerdsman.returnDevices.push('0x0017880104e45517');
|
||||
|
||||
const MQTT = require('./stub/mqtt');
|
||||
const settings = require('../lib/util/settings');
|
||||
const Controller = require('../lib/controller');
|
||||
@@ -54,7 +56,7 @@ describe('Device availability', () => {
|
||||
device.ping.mockImplementationOnce(() => {throw new Error('failed')});
|
||||
jest.advanceTimersByTime(11 * 1000);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenNthCalledWith(1,
|
||||
'zigbee2mqtt/bulb_color/availability',
|
||||
'offline',
|
||||
@@ -68,8 +70,8 @@ describe('Device availability', () => {
|
||||
await flushPromises();
|
||||
expect(logger.debug).toHaveBeenCalledTimes(3);
|
||||
expect(logger.debug).toHaveBeenCalledWith("Failed to ping 'bulb_color'");
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(4);
|
||||
expect(MQTT.publish).toHaveBeenNthCalledWith(3,
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenNthCalledWith(1,
|
||||
'zigbee2mqtt/bulb_color/availability',
|
||||
'offline',
|
||||
{ retain: true, qos: 0 },
|
||||
@@ -99,6 +101,21 @@ describe('Device availability', () => {
|
||||
expect(endpoint.read).toHaveBeenCalledWith('lightingColorCtrl', ['currentX', 'currentY', 'colorTemperature']);
|
||||
});
|
||||
|
||||
it('Should fail gracefully when quering state after reconnect fails', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
endpoint.read.mockClear();
|
||||
endpoint.read.mockImplementationOnce(() => {throw new Error('Device timedout')});
|
||||
device.ping.mockImplementationOnce(() => {throw new Error('failed')});
|
||||
logger.debug.mockClear();
|
||||
jest.advanceTimersByTime(11 * 2000);
|
||||
await flushPromises();
|
||||
|
||||
expect(endpoint.read).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.read).toHaveBeenCalledWith('genOnOff', ['onOff']);
|
||||
expect(logger.error).toHaveBeenCalledWith(`Failed to read state of 'bulb_color' after reconnect`);
|
||||
});
|
||||
|
||||
it('Shouldnt ping again when still pinging', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
device.ping.mockClear();
|
||||
@@ -171,15 +188,6 @@ describe('Device availability', () => {
|
||||
expect(endpoint.read).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should do nothing when receiving message from non-pingable device', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
const device = zigbeeHerdsman.devices.WXKG11LM;
|
||||
const payload = {device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10, cluster: 'genBasic', data: {modelId: device.modelID}};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should not do anything when message has no device', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
@@ -189,9 +197,23 @@ describe('Device availability', () => {
|
||||
expect(MQTT.publish.mock.calls.find((c) => c[0].includes('availability'))).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Should not ping blacklisted devices', async () => {
|
||||
it('Should not ping devices blacklisted by friendly name', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
settings.set(['advanced', 'availability_blacklist'], [device.ieeeAddr])
|
||||
settings.set(['advanced', 'availability_blacklist'], ['bulb_color'])
|
||||
await controller.stop();
|
||||
await flushPromises();
|
||||
controller = new Controller();
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
device.ping.mockClear();
|
||||
jest.advanceTimersByTime(11 * 1000);
|
||||
await flushPromises();
|
||||
expect(device.ping).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should not ping devices blacklisted by IEEE address', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
settings.set(['advanced', 'availability_blacklist'], [device.ieeeAddr]);
|
||||
await controller.stop();
|
||||
await flushPromises();
|
||||
controller = new Controller();
|
||||
@@ -216,6 +238,51 @@ describe('Device availability', () => {
|
||||
expect(device.ping).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should ping devices whitelisted by friendly name if availability_whitelist is set', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
settings.set(['advanced', 'availability_whitelist'], ['bulb_color']);
|
||||
await controller.stop();
|
||||
await flushPromises();
|
||||
controller = new Controller();
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
device.ping.mockClear();
|
||||
jest.advanceTimersByTime(11 * 1000);
|
||||
await flushPromises();
|
||||
expect(device.ping).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should ping devices whitelisted by IEEE address if availability_whitelist is set', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
settings.set(['advanced', 'availability_whitelist'], [device.ieeeAddr]);
|
||||
await controller.stop();
|
||||
await flushPromises();
|
||||
controller = new Controller();
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
device.ping.mockClear();
|
||||
jest.advanceTimersByTime(11 * 1000);
|
||||
await flushPromises();
|
||||
expect(device.ping).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should not ping non-whitelisted devices if availability_whitelist is set', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
getExtension().state[device.ieeeAddr] = false;
|
||||
settings.set(['advanced', 'availability_whitelist'], ['0x000b57fffec6a5b3'])
|
||||
await controller.stop();
|
||||
await flushPromises();
|
||||
controller = new Controller();
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
jest.advanceTimersByTime(11 * 1000);
|
||||
await flushPromises();
|
||||
expect(device.ping).toHaveBeenCalledTimes(0);
|
||||
MQTT.publish.mockClear();
|
||||
await zigbeeHerdsman.events.deviceAnnounce({device});
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should not read when device has no modelID and reconnects', async () => {
|
||||
const device = zigbeeHerdsman.devices.nomodel;
|
||||
getExtension().state[device.ieeeAddr] = true;
|
||||
@@ -233,4 +300,57 @@ describe('Device availability', () => {
|
||||
await flushPromises();
|
||||
expect(endpoint.read).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should stop pinging device when removed', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
device.ping.mockClear();
|
||||
const endpoint = device.getEndpoint(1);
|
||||
jest.advanceTimersByTime(11 * 1000);
|
||||
await flushPromises();
|
||||
expect(device.ping).toHaveBeenCalledTimes(1);
|
||||
settings.removeDevice(device.ieeeAddr);
|
||||
jest.advanceTimersByTime(11 * 1000);
|
||||
await flushPromises();
|
||||
expect(device.ping).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should publish availability when end device joins', async () => {
|
||||
const device = zigbeeHerdsman.devices.WXKG02LM;
|
||||
const payload = {device};
|
||||
MQTT.publish.mockClear();
|
||||
await zigbeeHerdsman.events.deviceJoined(payload);
|
||||
await flushPromises();
|
||||
expect(device.ping).toHaveBeenCalledTimes(0);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/button_double_key/availability',
|
||||
'online',
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Should mark non-pingable device as non-available when offline for longer than 24 hours', async () => {
|
||||
const device = zigbeeHerdsman.devices.remote;
|
||||
const defaultLastSeen = device.lastSeen;
|
||||
device.lastSeen = Date.now();
|
||||
MQTT.publish.mockClear();
|
||||
jest.advanceTimersByTime(1000 * 60 * 60 * 1); // 1 hours
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
device.lastSeen = device.lastSeen - (1000 * 60 * 60 * 25);
|
||||
jest.advanceTimersByTime(1000 * 60 * 60 * 1); // 1 hours
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/remote/availability',
|
||||
'offline',
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
|
||||
// Shouldn't do anything more when device is removed
|
||||
settings.removeDevice(device.ieeeAddr);
|
||||
jest.advanceTimersByTime(1000 * 60 * 60 * 1); // 1 hours
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
|
||||
device.lastSeen = defaultLastSeen;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ describe('Device bind', () => {
|
||||
endpoint.write.mockClear();
|
||||
endpoint.configureReporting.mockClear();
|
||||
endpoint.bind.mockClear();
|
||||
endpoint.unbind.mockClear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,4 +172,24 @@ describe('Device bind', () => {
|
||||
expect(endpoint.bind).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target);
|
||||
});
|
||||
|
||||
it('Should unbind from default_bind_group', async () => {
|
||||
const device = zigbeeHerdsman.devices.remote;
|
||||
const target = 'default_bind_group';
|
||||
const endpoint = device.getEndpoint(1);
|
||||
mockClear(device);
|
||||
MQTT.events.message('zigbee2mqtt/bridge/unbind/remote', target);
|
||||
await flushPromises();
|
||||
expect(endpoint.unbind).toHaveBeenCalledTimes(3);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith("genOnOff", 901);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith("genLevelCtrl", 901);
|
||||
expect(endpoint.unbind).toHaveBeenCalledWith("genScenes", 901);
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(3);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'default_bind_group', cluster: 'genScenes'}});
|
||||
expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'default_bind_group', cluster: 'genOnOff'}});
|
||||
expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'default_bind_group', cluster: 'genLevelCtrl'}});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const mocksClear = [MQTT.publish, logger.warn, logger.debug];
|
||||
|
||||
describe('Device receive', () => {
|
||||
describe('Device configure', () => {
|
||||
let controller;
|
||||
|
||||
expectRemoteConfigured = () => {
|
||||
@@ -17,6 +17,8 @@ describe('Device receive', () => {
|
||||
const endpoint1 = device.getEndpoint(1);
|
||||
expect(endpoint1.bind).toHaveBeenCalledTimes(2);
|
||||
expect(endpoint1.bind).toHaveBeenCalledWith('genOnOff', this.coordinatorEndoint);
|
||||
expect(endpoint1.bind).toHaveBeenCalledWith('genLevelCtrl', this.coordinatorEndoint);
|
||||
|
||||
const endpoint2 = device.getEndpoint(2);
|
||||
expect(endpoint2.write).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint2.write).toHaveBeenCalledWith("genBasic", {"49": {"type": 25, "value": 11}}, {"disableDefaultResponse": true, "manufacturerCode": 4107});
|
||||
@@ -54,6 +56,18 @@ describe('Device receive', () => {
|
||||
expectRemoteConfigured();
|
||||
});
|
||||
|
||||
it('Should re-configure when device rejoins', async () => {
|
||||
expectRemoteConfigured();
|
||||
const device = zigbeeHerdsman.devices.remote;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
await flushPromises();
|
||||
mockClear(device);
|
||||
const payload = {device};
|
||||
zigbeeHerdsman.events.deviceJoined(payload);
|
||||
await flushPromises();
|
||||
expectRemoteConfigured();
|
||||
});
|
||||
|
||||
it('Should not configure twice', async () => {
|
||||
expectRemoteConfigured();
|
||||
const device = zigbeeHerdsman.devices.remote;
|
||||
@@ -76,6 +90,27 @@ describe('Device receive', () => {
|
||||
expectRemoteConfigured();
|
||||
});
|
||||
|
||||
it('Should allow to reconfigure manually', async () => {
|
||||
expectRemoteConfigured();
|
||||
mockClear(zigbeeHerdsman.devices.remote);
|
||||
expectRemoteNotConfigured();
|
||||
await MQTT.events.message('zigbee2mqtt/bridge/configure', 'remote');
|
||||
await flushPromises();
|
||||
expectRemoteConfigured();
|
||||
});
|
||||
|
||||
it('Shouldnt manually reconfigure when device does not exist', async () => {
|
||||
await MQTT.events.message('zigbee2mqtt/bridge/configure', 'remote_random_non_existing');
|
||||
await flushPromises();
|
||||
expect(logger.error).toHaveBeenCalledWith(`Device 'remote_random_non_existing' does not exist`);
|
||||
});
|
||||
|
||||
it('Should skip reconfigure when device does not require this', async () => {
|
||||
await MQTT.events.message('zigbee2mqtt/bridge/configure', 'bulb');
|
||||
await flushPromises();
|
||||
expect(logger.warn).toHaveBeenCalledWith(`Skipping configure of 'bulb', device does not require this.`)
|
||||
});
|
||||
|
||||
it('Should not configure when interviewing', async () => {
|
||||
const device = zigbeeHerdsman.devices.remote;
|
||||
delete device.meta.configured;
|
||||
|
||||
@@ -15,6 +15,7 @@ const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
const mockOnEvent = jest.fn();
|
||||
const mappedLivolo = zigbeeHerdsmanConverters.findByZigbeeModel(zigbeeHerdsman.devices.LIVOLO.modelID);
|
||||
mappedLivolo.onEvent = mockOnEvent;
|
||||
zigbeeHerdsmanConverters.onEvent = jest.fn();
|
||||
|
||||
describe('Device event', () => {
|
||||
let controller;
|
||||
@@ -27,6 +28,7 @@ describe('Device event', () => {
|
||||
controller = new Controller();
|
||||
await controller.start();
|
||||
mocksClear.forEach((m) => m.mockClear());
|
||||
zigbeeHerdsmanConverters.onEvent.mockClear();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
@@ -59,4 +61,15 @@ describe('Device event', () => {
|
||||
expect(call[1]).toStrictEqual({device})
|
||||
expect(call[2]).toBe(device);
|
||||
});
|
||||
|
||||
it('Should call index onEvent with zigbee event', async () => {
|
||||
zigbeeHerdsmanConverters.onEvent.mockClear();
|
||||
await zigbeeHerdsman.events.deviceAnnounce({device});
|
||||
await flushPromises();
|
||||
expect(zigbeeHerdsmanConverters.onEvent).toHaveBeenCalledTimes(1);
|
||||
const call = zigbeeHerdsmanConverters.onEvent.mock.calls[0];
|
||||
expect(call[0]).toBe('deviceAnnounce')
|
||||
expect(call[1]).toStrictEqual({device})
|
||||
expect(call[2]).toBe(device);
|
||||
});
|
||||
});
|
||||
|
||||
Regular → Executable
+60
-20
@@ -110,6 +110,29 @@ describe('Device receive', () => {
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false});
|
||||
});
|
||||
|
||||
it('Should debounce messages only with the same payload values for provided debounce_ignore keys', async () => {
|
||||
jest.useFakeTimers();
|
||||
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
|
||||
settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1);
|
||||
settings.set(['devices', device.ieeeAddr, 'debounce_ignore'], ['temperature']);
|
||||
const tempMsg = {data: {measuredValue: 8}, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 13};
|
||||
await zigbeeHerdsman.events.message(tempMsg);
|
||||
const pressureMsg = {data: {measuredValue: 2}, cluster: 'msPressureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 13};
|
||||
await zigbeeHerdsman.events.message(pressureMsg);
|
||||
const tempMsg2 = {data: {measuredValue: 7}, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 13};
|
||||
await zigbeeHerdsman.events.message(tempMsg2);
|
||||
const humidityMsg = {data: {measuredValue: 3}, cluster: 'msRelativeHumidity', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 13};
|
||||
await zigbeeHerdsman.events.message(humidityMsg);
|
||||
await flushPromises();
|
||||
jest.advanceTimersByTime(50);
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, pressure: 2, linkquality: 13});
|
||||
jest.runAllTimers();
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({temperature: 0.07, pressure: 2, humidity: 0.03, linkquality: 13});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with 1 precision', async () => {
|
||||
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
|
||||
settings.set(['devices', device.ieeeAddr, 'temperature_precision'], 1);
|
||||
@@ -163,6 +186,25 @@ describe('Device receive', () => {
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false});
|
||||
});
|
||||
|
||||
it('WSDCGQ11LM pressure precision from non ZCL properties', async () => {
|
||||
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
|
||||
settings.set(['devices', device.ieeeAddr, 'temperature_precision'], 1);
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
let payload = {data: {"65281":{"1":2985,"4":5032,"5":9,"6":[0,1],"10":0,"100":2345,"101":4608,"102":91552}}, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({"battery":91,"voltage":2985,"temperature":23.5,"humidity":46.08,"pressure":915.5,"linkquality":10});
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
payload = {data: {"16":9354,"20":-1,"measuredValue":915}, cluster: 'msPressureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({"battery":91,"voltage":2985,"temperature":23.5,"humidity":46.08,"pressure":935.4,"linkquality":10});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with voltage 3010', async () => {
|
||||
const device = zigbeeHerdsman.devices.WXKG02LM;
|
||||
const data = {'65281': {'1': 3010}}
|
||||
@@ -219,7 +261,7 @@ describe('Device receive', () => {
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/occupancy_sensor');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'battery': 100, 'illuminance': 381, 'voltage': 3045, linkquality: 10});
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'battery': 100, 'illuminance': 381, "illuminance_lux": 381, 'voltage': 3045, linkquality: 10});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
});
|
||||
|
||||
@@ -362,29 +404,27 @@ describe('Device receive', () => {
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
expect(logger.warn).toHaveBeenCalledWith('No converter available for \'ZNCZ02LM\' with cluster \'genBinaryOutput\' and type \'attributeReport\' and data \'{"inactiveText":"hello"}\'');
|
||||
expect(logger.debug).toHaveBeenCalledWith('No converter available for \'ZNCZ02LM\' with cluster \'genBinaryOutput\' and type \'attributeReport\' and data \'{"inactiveText":"hello"}\'');
|
||||
});
|
||||
|
||||
it('Should not log when message is from supported device but has no converters is readResponse', async () => {
|
||||
const device = zigbeeHerdsman.devices.ZNCZ02LM;
|
||||
const data = {inactiveText: 'hello'};
|
||||
const payload = {data, cluster: 'genBinaryOutput', device, endpoint: device.getEndpoint(1), type: 'readResponse', linkquality: 20};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
expect(logger.warn).toBeCalledTimes(0);
|
||||
});
|
||||
it('Should report correct energy and power values for different versions of SP600', async () => {
|
||||
// https://github.com/Koenkk/zigbee-herdsman-converters/issues/915, OLD and NEW use different date code
|
||||
// divisor of OLD is not correct and therefore underreports by factor 10.
|
||||
const data = {instantaneousDemand:496,currentSummDelivered:[0,6648]}
|
||||
|
||||
it('Should allow to calibrate temperature values', async () => {
|
||||
const device = zigbeeHerdsman.devices.WSDCGQ11LM;
|
||||
settings.set(['devices', device.ieeeAddr, 'temperature_calibration'], 2);
|
||||
const data = {measuredValue: -85}
|
||||
const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
const SP600_NEW = zigbeeHerdsman.devices.SP600_NEW;
|
||||
await zigbeeHerdsman.events.message({data, cluster: 'seMetering', device: SP600_NEW, endpoint: SP600_NEW.getEndpoint(1), type: 'attributeReport', linkquality: 10});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 1.15, linkquality: 10});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false});
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_NEW');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({energy: 0.66, power: 49.6, linkquality: 10});
|
||||
|
||||
MQTT.publish.mockClear();
|
||||
const SP600_OLD = zigbeeHerdsman.devices.SP600_OLD;
|
||||
await zigbeeHerdsman.events.message({data, cluster: 'seMetering', device: SP600_OLD, endpoint: SP600_OLD.getEndpoint(1), type: 'attributeReport', linkquality: 10});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_OLD');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({energy: 6.648, power: 496, linkquality: 10});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ zigbeeHerdsman.returnDevices.push('0x0017880104e45553');
|
||||
zigbeeHerdsman.returnDevices.push('0x0017880104e45559');
|
||||
zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b4');
|
||||
zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b7');
|
||||
zigbeeHerdsman.returnDevices.push('0x90fd9ffffe4b64ax');
|
||||
const MQTT = require('./stub/mqtt');
|
||||
const settings = require('../lib/util/settings');
|
||||
const Controller = require('../lib/controller');
|
||||
@@ -29,8 +30,8 @@ describe('Device report', () => {
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', coordinatorEndpoint);
|
||||
expect(endpoint.configureReporting).toHaveBeenCalledTimes(3);
|
||||
expect(endpoint.configureReporting).toHaveBeenCalledWith('genOnOff', [{"attribute": "onOff", "maximumReportInterval": 300, "minimumReportInterval": 0, "reportableChange": 0}]);
|
||||
expect(endpoint.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [{"attribute": "currentLevel", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 0}]);
|
||||
expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [{"attribute": "colorTemperature", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 0}, {"attribute": "currentX", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 0}, {"attribute": "currentY", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 0}]);
|
||||
expect(endpoint.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [{"attribute": "currentLevel", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 1}]);
|
||||
expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [{"attribute": "colorTemperature", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 1}, {"attribute": "currentX", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 1}, {"attribute": "currentY", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 1}]);
|
||||
}
|
||||
|
||||
mockClear = (device) => {
|
||||
@@ -197,4 +198,17 @@ describe('Device report', () => {
|
||||
// Should only call Hue bulb, not e.g. tradfri
|
||||
expect(zigbeeHerdsman.devices.bulb.getEndpoint(1).read).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should not configure reporting for the ZNLDP12LM closuresWindowCovering as it is ignored', async () => {
|
||||
const device = zigbeeHerdsman.devices.ZNLDP12LM;
|
||||
const coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1);
|
||||
const endpoint = device.getEndpoint(1);
|
||||
delete device.meta.reporting;
|
||||
await flushPromises();
|
||||
expect(endpoint.bind).toHaveBeenCalledTimes(3);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', coordinatorEndpoint);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', coordinatorEndpoint);
|
||||
expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', coordinatorEndpoint);
|
||||
expect(endpoint.configureReporting).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
+190
-7
@@ -168,6 +168,19 @@ describe('Entity publish', () => {
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
});
|
||||
|
||||
it('Should publish messages to zigbee devices to non default-ep with state_[EP]', async () => {
|
||||
const device = zigbeeHerdsman.devices.QBKG03LM;
|
||||
const endpoint = device.getEndpoint(3);
|
||||
await MQTT.events.message('zigbee2mqtt/wall_switch_double/set', JSON.stringify({state_right: 'OFF'}));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "off", {}, {});
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wall_switch_double');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state_right: 'OFF'});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
});
|
||||
|
||||
it('Should publish messages to zigbee devices with color xy', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
@@ -458,6 +471,18 @@ describe('Entity publish', () => {
|
||||
expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}, {});
|
||||
});
|
||||
|
||||
it('Should parse set with color attribute topic', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set/color', '#64C80A');
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 17806, colory: 43155, transtime: 0}, {});
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 156, color: {x: 0.2717, y: 0.6585}});
|
||||
});
|
||||
|
||||
it('Should parse set with ieeeAddr topic', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
@@ -493,6 +518,16 @@ describe('Entity publish', () => {
|
||||
expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}, {});
|
||||
});
|
||||
|
||||
it('Should parse set with number at the end of friendly_name and postfix', async () => {
|
||||
const device = zigbeeHerdsman.devices.QBKG03LM;
|
||||
settings.set(['devices', device.ieeeAddr, 'friendly_name'], 'ground_floor/kitchen/wall_switch/2');
|
||||
const endpoint = device.getEndpoint(2);
|
||||
await MQTT.events.message('zigbee2mqtt/ground_floor/kitchen/wall_switch/2/left/set', JSON.stringify({state: 'ON'}));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}, {});
|
||||
});
|
||||
|
||||
it('Should not publish messages to zigbee devices when payload is invalid', async () => {
|
||||
const device = zigbeeHerdsman.devices.QBKG03LM;
|
||||
const endpoint = device.getEndpoint(2);
|
||||
@@ -626,6 +661,19 @@ describe('Entity publish', () => {
|
||||
expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "off", {}, {}]);
|
||||
});
|
||||
|
||||
it('Should allow to set color via hue and saturation', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
const payload = {"color":{"hue":250, "saturation":50}};
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "enhancedMoveToHueAndSaturation", {"direction": 0, "enhancehue": 45510.416666666664, "saturation": 127, "transtime": 0,}, {}]);
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({"color":{"hue":250,"saturation":50}});
|
||||
});
|
||||
|
||||
it('ZNCLDJ11LM open', async () => {
|
||||
const device = zigbeeHerdsman.devices.ZNCLDJ11LM;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
@@ -670,11 +718,21 @@ describe('Entity publish', () => {
|
||||
it('Should turn device on with on/off when transition is provided', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
const payload = {state: 'ON', transition: 1};
|
||||
const payload = {state: 'ON', transition: 3};
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}, {}]);
|
||||
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 255, transtime: 30}, {}]);
|
||||
});
|
||||
|
||||
it('Should turn device on with on/off with transition when transition 0 is provided', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
const payload = {state: 'ON', transition: 0};
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 255, transtime: 0}, {}]);
|
||||
});
|
||||
|
||||
it('Should turn device off with onOff on off with transition', async () => {
|
||||
@@ -684,12 +742,44 @@ describe('Entity publish', () => {
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "off", {}, {}]);
|
||||
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 10}, {}]);
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'});
|
||||
});
|
||||
|
||||
it('When device is turned off and on with transition with report enabled it should restore correct brightness', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
// Set initial brightness in state
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({brightness: 200}));
|
||||
await flushPromises();
|
||||
endpoint.command.mockClear();
|
||||
MQTT.publish.mockClear();
|
||||
|
||||
// Turn off
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({state: 'OFF', transition: 3}));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 30}, {}]);
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0]).toEqual(["zigbee2mqtt/bulb_color", JSON.stringify({state: 'OFF', brightness: 200}), {"qos": 0, "retain": false}, expect.any(Function)]);
|
||||
|
||||
// Bulb reports brightness while decreasing brightness
|
||||
await zigbeeHerdsman.events.message({data: {currentLevel: 1}, cluster: 'genLevelCtrl', device, endpoint, type: 'attributeReport', linkquality: 10});
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(2);
|
||||
expect(MQTT.publish.mock.calls[1]).toEqual(["zigbee2mqtt/bulb_color", JSON.stringify({state: 'OFF', brightness: 1, linkquality: 10}), {"qos": 0, "retain": false}, expect.any(Function)]);
|
||||
|
||||
// Turn on again
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({state: 'ON', transition: 3}));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(2);
|
||||
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 30}, {}]);
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(3);
|
||||
expect(MQTT.publish.mock.calls[2]).toEqual(["zigbee2mqtt/bulb_color", JSON.stringify({state: 'ON', brightness: 200, linkquality: 10}), {"qos": 0, "retain": false}, expect.any(Function)]);
|
||||
});
|
||||
|
||||
it('Home Assistant: should set state', async () => {
|
||||
settings.set(['homeassistant'], true);
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
@@ -752,6 +842,30 @@ describe('Entity publish', () => {
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.41, y: 0.25}, color_temp: 150, state: 'ON'});
|
||||
});
|
||||
|
||||
it('Should publish correct state on toggle command to zigbee bulb', async () => {
|
||||
const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1);
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({state: 'TOGGLE'}));
|
||||
await flushPromises();
|
||||
|
||||
// At this point the bulb has no state yet, so we cannot determine the next state and therefore shouldn't publish it to MQTT.
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "toggle", {}, {});
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
|
||||
// Turn bulb off so that the bulb gets a state.
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({state: 'OFF'}));
|
||||
await flushPromises();
|
||||
|
||||
// Toggle again, now that we have state it should publish state ON
|
||||
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'});
|
||||
expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
});
|
||||
|
||||
it('Should publish messages with options disableDefaultResponse', async () => {
|
||||
const device = zigbeeHerdsman.devices.GLEDOPTO1112;
|
||||
const endpoint = device.getEndpoint(11);
|
||||
@@ -776,13 +890,29 @@ describe('Entity publish', () => {
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
});
|
||||
|
||||
it('Should publish brightness_move up to zigbee devices', async () => {
|
||||
const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1);
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({brightness_move: -40}));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "move", {"movemode": 1, "rate": 40}, {});
|
||||
});
|
||||
|
||||
it('Should publish brightness_move down to zigbee devices', async () => {
|
||||
const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1);
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({brightness_move: 30}));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "move", {"movemode": 0, "rate": 30}, {});
|
||||
});
|
||||
|
||||
it('HS2WD-E burglar warning', async () => {
|
||||
const endpoint = zigbeeHerdsman.devices.HS2WD.getEndpoint(1);
|
||||
const payload = {warning: {duration: 100, mode: 'burglar', strobe: true, level: 'high'}};
|
||||
await MQTT.events.message('zigbee2mqtt/siren/set', JSON.stringify(payload));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 22, "warningduration": 100}, {});
|
||||
expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 22, "warningduration": 100}, {disableDefaultResponse: true});
|
||||
});
|
||||
|
||||
it('HS2WD-E emergency warning', async () => {
|
||||
@@ -791,7 +921,7 @@ describe('Entity publish', () => {
|
||||
await MQTT.events.message('zigbee2mqtt/siren/set', JSON.stringify(payload));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 51, "warningduration": 10}, {});
|
||||
expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 51, "warningduration": 10}, {disableDefaultResponse: true});
|
||||
});
|
||||
|
||||
it('HS2WD-E emergency without level', async () => {
|
||||
@@ -800,7 +930,7 @@ describe('Entity publish', () => {
|
||||
await MQTT.events.message('zigbee2mqtt/siren/set', JSON.stringify(payload));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 49, "warningduration": 10}, {});
|
||||
expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 49, "warningduration": 10}, {disableDefaultResponse: true});
|
||||
});
|
||||
|
||||
it('HS2WD-E wrong payload (should use defaults)', async () => {
|
||||
@@ -809,7 +939,7 @@ describe('Entity publish', () => {
|
||||
await MQTT.events.message('zigbee2mqtt/siren/set', JSON.stringify(payload));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 53, "warningduration": 10}, {});
|
||||
expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 53, "warningduration": 10}, {disableDefaultResponse: true});
|
||||
});
|
||||
|
||||
it('Shouldnt do anythign when device is not supported', async () => {
|
||||
@@ -818,4 +948,57 @@ describe('Entity publish', () => {
|
||||
await flushPromises();
|
||||
expectNothingPublished();
|
||||
});
|
||||
|
||||
it('Should publish state to roller shutter', async () => {
|
||||
const endpoint = zigbeeHerdsman.devices.roller_shutter.getEndpoint(1);
|
||||
await MQTT.events.message('zigbee2mqtt/roller_shutter/set', JSON.stringify({state: 'OPEN'}));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevel", {"level": "255", "transtime": 0}, {});
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/roller_shutter');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({position: 100});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
});
|
||||
|
||||
it('Should publish to MKS-CM-W5', async () => {
|
||||
const device = zigbeeHerdsman.devices['MKS-CM-W5'];
|
||||
const endpoint = device.getEndpoint(1);
|
||||
await MQTT.events.message('zigbee2mqtt/MKS-CM-W5/l3/set', JSON.stringify({state: 'ON'}));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(1);
|
||||
expect(endpoint.command).toHaveBeenCalledWith("manuSpecificTuyaDimmer", "setData", {data: [1,1], dp: 259, fn: 0, status: 0, transid: 16}, {disableDefaultResponse: true});
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/MKS-CM-W5');
|
||||
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state_l3: 'ON'});
|
||||
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
|
||||
});
|
||||
|
||||
it('Should publish separate genOnOff to GL-S-007ZS when setting state and brightness as bulb doesnt turn on with moveToLevelWithOnOff', async () => {
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/2757
|
||||
jest.useFakeTimers();
|
||||
const device = zigbeeHerdsman.devices['GL-S-007ZS'];
|
||||
const endpoint = device.getEndpoint(1);
|
||||
await MQTT.events.message('zigbee2mqtt/GL-S-007ZS/set', JSON.stringify({state: 'ON', brightness: 20}));
|
||||
await flushPromises();
|
||||
jest.runAllTimers();
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(2);
|
||||
expect(endpoint.command.mock.calls[0]).toEqual([ 'genOnOff', 'on', {}, {} ]);
|
||||
expect(endpoint.command.mock.calls[1]).toEqual([ 'genLevelCtrl', 'moveToLevelWithOnOff', { level: 20, transtime: 0 }, {} ]);
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish.mock.calls[0]).toEqual([ 'zigbee2mqtt/GL-S-007ZS', '{"state":"ON","brightness":20}', { qos: 0, retain: false }, expect.any(Function)]);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('Should log as error when setting property with no defined converter', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
const endpoint = device.getEndpoint(1);
|
||||
const payload = {'brightness_move': 20};
|
||||
logger.error.mockClear();
|
||||
await MQTT.events.message('zigbee2mqtt/bulb_color/get', JSON.stringify(payload));
|
||||
await flushPromises();
|
||||
expect(endpoint.command).toHaveBeenCalledTimes(0);
|
||||
expect(logger.error).toHaveBeenCalledWith("No converter available for 'get' 'brightness_move' (20)");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
|
||||
const flushPromises = () => new Promise(setImmediate);
|
||||
const MQTT = require('./stub/mqtt');
|
||||
const Controller = require('../lib/controller');
|
||||
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
describe('HomeAssistant extension', () => {
|
||||
beforeEach(async () => {
|
||||
@@ -22,7 +21,7 @@ describe('HomeAssistant extension', () => {
|
||||
it('Should have mapping for all devices supported by zigbee-herdsman-converters', () => {
|
||||
const missing = [];
|
||||
const HomeAssistant = require('../lib/extension/homeassistant');
|
||||
const ha = new HomeAssistant(null, null, null, null);
|
||||
const ha = new HomeAssistant(null, null, null, null, {on: () => {}});
|
||||
|
||||
require('zigbee-herdsman-converters').devices.forEach((d) => {
|
||||
if (!ha._getMapping()[d.model]) {
|
||||
@@ -141,7 +140,8 @@ describe('HomeAssistant extension', () => {
|
||||
);
|
||||
|
||||
payload = {
|
||||
'unit_of_measurement': '-',
|
||||
'icon': 'mdi:signal',
|
||||
'unit_of_measurement': 'lqi',
|
||||
'value_template': '{{ value_json.linkquality }}',
|
||||
'state_topic': 'zigbee2mqtt/weather_sensor',
|
||||
'json_attributes_topic': 'zigbee2mqtt/weather_sensor',
|
||||
@@ -416,6 +416,7 @@ describe('HomeAssistant extension', () => {
|
||||
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',
|
||||
unique_id: '0x0017880104e45551_cover_zigbee2mqtt',
|
||||
device:
|
||||
@@ -636,4 +637,86 @@ describe('HomeAssistant extension', () => {
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should clear discovery when device is removed', async () => {
|
||||
controller = new Controller(false);
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/config/remove', 'weather_sensor');
|
||||
await flushPromises();
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/sensor/0x0017880104e45522/temperature/config',
|
||||
null,
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/sensor/0x0017880104e45522/humidity/config',
|
||||
null,
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/sensor/0x0017880104e45522/pressure/config',
|
||||
null,
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/sensor/0x0017880104e45522/battery/config',
|
||||
null,
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/sensor/0x0017880104e45522/linkquality/config',
|
||||
null,
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('Should not clear discovery when unsupported device is removed', async () => {
|
||||
controller = new Controller(false);
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
MQTT.publish.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/config/remove', 'unsupported2');
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should discover update_available sensor when device supports it', async () => {
|
||||
controller = new Controller(false);
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
const payload = {
|
||||
"payload_on":true,
|
||||
"payload_off":false,
|
||||
"value_template":"{{ value_json.update_available}}",
|
||||
"state_topic":"zigbee2mqtt/bulb",
|
||||
"json_attributes_topic":"zigbee2mqtt/bulb",
|
||||
"name":"bulb_update_available",
|
||||
"unique_id":"0x000b57fffec6a5b2_update_available_zigbee2mqtt",
|
||||
"device":{
|
||||
"identifiers":[
|
||||
"zigbee2mqtt_0x000b57fffec6a5b2"
|
||||
],
|
||||
"name":"bulb",
|
||||
'sw_version': this.version,
|
||||
"model":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)",
|
||||
"manufacturer":"IKEA"
|
||||
},
|
||||
"availability_topic":"zigbee2mqtt/bridge/state"
|
||||
};
|
||||
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'homeassistant/binary_sensor/0x000b57fffec6a5b2/update_available/config',
|
||||
JSON.stringify(payload),
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+65
-5
@@ -1,20 +1,41 @@
|
||||
const tmp = require('tmp');
|
||||
const data = require('./stub/data');
|
||||
const dir = tmp.dirSync();
|
||||
const settings = require('../lib/util/settings');
|
||||
settings.set(['advanced', 'log_directory'], dir.name + '/%TIMESTAMP%');
|
||||
const logger = require('../lib/util/logger.js');
|
||||
let settings;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const data = require('./stub/data');
|
||||
let stdOutWriteOriginal;
|
||||
const rimraf = require('rimraf');
|
||||
|
||||
describe('Logger', () => {
|
||||
beforeEach(async () => {
|
||||
data.writeDefaultConfiguration();
|
||||
jest.resetModules();
|
||||
settings = require('../lib/util/settings');
|
||||
settings.set(['advanced', 'log_directory'], dir.name + '/%TIMESTAMP%');
|
||||
settings._reRead();
|
||||
stdOutWriteOriginal = console._stdout.write;
|
||||
console._stdout.write = () => {};
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
console._stdout.write = stdOutWriteOriginal;
|
||||
});
|
||||
|
||||
it('Create log directory', () => {
|
||||
const logger = require('../lib/util/logger.js');
|
||||
const dirs = fs.readdirSync(dir.name);
|
||||
expect(dirs.length).toBe(1);
|
||||
});
|
||||
|
||||
it('Should cleanup', () => {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const logger = require('../lib/util/logger.js');
|
||||
|
||||
for (const d of fs.readdirSync(dir.name)) {
|
||||
rimraf.sync(path.join(dir.name, d));
|
||||
}
|
||||
|
||||
for (let i = 0; i < 21; i++) {
|
||||
fs.mkdirSync(path.join(dir.name, `log_${i}`));
|
||||
}
|
||||
|
||||
@@ -24,6 +45,7 @@ describe('Logger', () => {
|
||||
})
|
||||
|
||||
it('Should not cleanup when there is no timestamp set', () => {
|
||||
const logger = require('../lib/util/logger.js');
|
||||
for (let i = 30; i < 40; i++) {
|
||||
fs.mkdirSync(path.join(dir.name, `log_${i}`));
|
||||
}
|
||||
@@ -35,7 +57,45 @@ describe('Logger', () => {
|
||||
})
|
||||
|
||||
it('Set and get log level', () => {
|
||||
const logger = require('../lib/util/logger.js');
|
||||
logger.setLevel('debug');
|
||||
expect(logger.getLevel()).toBe('debug');
|
||||
});
|
||||
|
||||
it('Logger should be console and file by default', () => {
|
||||
const logger = require('../lib/util/logger.js');
|
||||
const pipes = logger._readableState.pipes;
|
||||
expect(pipes.length).toBe(2);
|
||||
expect(pipes[0].constructor.name).toBe('Console');
|
||||
expect(pipes[0].silent).toBe(false);
|
||||
expect(pipes[1].constructor.name).toBe('File');
|
||||
expect(pipes[1].dirname.startsWith(dir.name)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Logger can be file only', () => {
|
||||
settings.set(['advanced', 'log_output'], ['file']);
|
||||
const logger = require('../lib/util/logger.js');
|
||||
const pipes = logger._readableState.pipes;
|
||||
expect(pipes.length).toBe(2);
|
||||
expect(pipes[0].constructor.name).toBe('Console');
|
||||
expect(pipes[0].silent).toBe(true);
|
||||
expect(pipes[1].constructor.name).toBe('File');
|
||||
expect(pipes[1].dirname.startsWith(dir.name)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Logger can be console only', () => {
|
||||
settings.set(['advanced', 'log_output'], ['console']);
|
||||
const logger = require('../lib/util/logger.js');
|
||||
const pipes = logger._readableState.pipes;
|
||||
expect(pipes.constructor.name).toBe('Console');
|
||||
expect(pipes.silent).toBe(false);
|
||||
});
|
||||
|
||||
it('Logger can be nothing', () => {
|
||||
settings.set(['advanced', 'log_output'], []);
|
||||
const logger = require('../lib/util/logger.js');
|
||||
const pipes = logger._readableState.pipes;
|
||||
expect(pipes.constructor.name).toBe('Console');
|
||||
expect(pipes.silent).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
const data = require('./stub/data');
|
||||
const logger = require('./stub/logger');
|
||||
const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
|
||||
const MQTT = require('./stub/mqtt');
|
||||
const settings = require('../lib/util/settings');
|
||||
const Controller = require('../lib/controller');
|
||||
const flushPromises = () => new Promise(setImmediate);
|
||||
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
||||
|
||||
describe('OTA update', () => {
|
||||
let controller;
|
||||
|
||||
mockClear = (mapped) => {
|
||||
mapped.ota.updateToLatest = jest.fn();
|
||||
mapped.ota.isUpdateAvailable = jest.fn();
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
data.writeDefaultConfiguration();
|
||||
settings._reRead();
|
||||
data.writeEmptyState();
|
||||
controller = new Controller();
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
MQTT.publish.mockClear();
|
||||
});
|
||||
|
||||
it('Should subscribe to nested topics', async () => {
|
||||
expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/ota_update/check');
|
||||
expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/ota_update/update');
|
||||
});
|
||||
|
||||
it('Should OTA update a device', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const endpoint = device.endpoints[0];
|
||||
let count = 0;
|
||||
endpoint.read.mockImplementation(() => {
|
||||
count++;
|
||||
return {swBuildId: count, dateCode: '2019010' + count}
|
||||
});
|
||||
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
|
||||
mockClear(mapped);
|
||||
logger.info.mockClear();
|
||||
logger.error.mockClear();
|
||||
device.save.mockClear();
|
||||
mapped.ota.updateToLatest.mockImplementationOnce((a, b, onUpdate) => {
|
||||
onUpdate(0, null);
|
||||
onUpdate(10, 3600);
|
||||
});
|
||||
|
||||
MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb');
|
||||
await flushPromises();
|
||||
expect(logger.info).toHaveBeenCalledWith(`Updating 'bulb' to latest firmware`);
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(0);
|
||||
expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(1);
|
||||
expect(mapped.ota.updateToLatest).toHaveBeenCalledWith(device, logger, expect.any(Function));
|
||||
expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 0%`);
|
||||
expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 10%, +- 60 minutes remaining`);
|
||||
expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb', from '{"softwareBuildID":1,"dateCode":"20190101"}' to '{"softwareBuildID":2,"dateCode":"20190102"}'`);
|
||||
expect(logger.error).toHaveBeenCalledTimes(0);
|
||||
expect(device.save).toHaveBeenCalledTimes(1);
|
||||
expect(device.dateCode).toBe('20190102');
|
||||
expect(device.softwareBuildID).toBe(2);
|
||||
});
|
||||
|
||||
it('Should handle when OTA update fails', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const endpoint = device.endpoints[0];
|
||||
endpoint.read.mockImplementation(() => {return {swBuildId: 1, dateCode: '2019010'}});
|
||||
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
|
||||
mockClear(mapped);
|
||||
logger.info.mockClear();
|
||||
logger.error.mockClear();
|
||||
device.save.mockClear();
|
||||
mapped.ota.updateToLatest.mockImplementationOnce((a, b, onUpdate) => {
|
||||
throw new Error('Update failed');
|
||||
});
|
||||
|
||||
MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb');
|
||||
await flushPromises();
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
expect(logger.error).toHaveBeenCalledWith(`Update of 'bulb' failed (Update failed)`);
|
||||
});
|
||||
|
||||
it('Should be able to check if OTA update is available', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
|
||||
mockClear(mapped);
|
||||
|
||||
logger.info.mockClear();
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(false);
|
||||
MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb');
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0);
|
||||
expect(logger.info).toHaveBeenCalledWith(`No update available for 'bulb'`);
|
||||
|
||||
logger.info.mockClear();
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(true);
|
||||
MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb');
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(2);
|
||||
expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0);
|
||||
expect(logger.info).toHaveBeenCalledWith(`Update available for 'bulb'`);
|
||||
});
|
||||
|
||||
it('Should handle if OTA update check fails', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
|
||||
mockClear(mapped);
|
||||
logger.error.mockClear();
|
||||
mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {throw new Error('RF singals disturbed because of dogs barking')});
|
||||
|
||||
MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb');
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0);
|
||||
expect(logger.error).toHaveBeenCalledWith(`Failed to check if update available for 'bulb' (RF singals disturbed because of dogs barking)`);
|
||||
});
|
||||
|
||||
it('Should not check for OTA when device does not support it', async () => {
|
||||
MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb_color_2');
|
||||
await flushPromises();
|
||||
expect(logger.error).toHaveBeenCalledWith(`Device 'bulb_color_2' does not support OTA updates`);
|
||||
});
|
||||
|
||||
it('Should refuse to check/update when already in progress', async () => {
|
||||
jest.useFakeTimers();
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
|
||||
mockClear(mapped);
|
||||
|
||||
logger.info.mockClear();
|
||||
mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {
|
||||
return new Promise((resolve, reject) => {setTimeout(() => resolve(), 99999)})
|
||||
});
|
||||
MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb');
|
||||
await flushPromises();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb');
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
expect(logger.error).toHaveBeenCalledWith(`Update or check already in progress for 'bulb', skipping...`);
|
||||
jest.runAllTimers();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it('Shouldnt crash when read modelID after OTA update fails', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const endpoint = device.endpoints[0];
|
||||
let count = 0;
|
||||
endpoint.read.mockImplementation(() => {
|
||||
if (count === 1) throw new Error('Failed!')
|
||||
count++;
|
||||
return {swBuildId: 1, dateCode: '2019010'}
|
||||
});
|
||||
|
||||
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
|
||||
mockClear(mapped);
|
||||
logger.info.mockClear();
|
||||
MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb');
|
||||
await flushPromises();
|
||||
expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb'`);
|
||||
});
|
||||
|
||||
it('Should check for update when device requests it', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
const data = {imageType: 12382};
|
||||
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
|
||||
mockClear(mapped);
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(true);
|
||||
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10};
|
||||
logger.info.mockClear();
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, logger, {"imageType": 12382});
|
||||
expect(logger.info).toHaveBeenCalledWith(`Update available for 'bulb'`)
|
||||
|
||||
// Should not request again when device asks again after a short time
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
|
||||
|
||||
const extension = controller.extensions.find((e) => e.constructor.name === 'OTAUpdate');
|
||||
extension.lastChecked = {};
|
||||
logger.info.mockClear();
|
||||
mapped.ota.isUpdateAvailable.mockReturnValueOnce(false);
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(logger.info).not.toHaveBeenCalledWith(`Update available for 'bulb'`)
|
||||
});
|
||||
});
|
||||
+29
-1
@@ -1,6 +1,7 @@
|
||||
require('./stub/logger');
|
||||
require('./stub/data');
|
||||
const data = require('../lib/util/data.js');
|
||||
const utils = require('../lib/util/utils.js');
|
||||
const settings = require('../lib/util/settings.js');
|
||||
const fs = require('fs');
|
||||
const configurationFile = data.joinPath('configuration.yaml');
|
||||
@@ -91,7 +92,7 @@ describe('Settings', () => {
|
||||
settings.getDevice('0x12345678');
|
||||
});
|
||||
|
||||
it('onlythis Should read MQTT username asn password form a separate file', () => {
|
||||
it('Should read MQTT username and password form a separate file', () => {
|
||||
const contentConfiguration = {
|
||||
mqtt: {
|
||||
server: 'my.mqtt.server',
|
||||
@@ -511,6 +512,18 @@ describe('Settings', () => {
|
||||
}).toThrowError(`Duplicate friendly_name 'myname' found`);
|
||||
});
|
||||
|
||||
it('Configuration shouldnt be valid when friendly_name is a postfix', async () => {
|
||||
write(configurationFile, {
|
||||
devices: {'0x0017880104e45519': {friendly_name: 'left', retain: false}},
|
||||
});
|
||||
|
||||
settings._reRead();
|
||||
|
||||
expect(() => {
|
||||
settings.validate();
|
||||
}).toThrowError(`Following friendly_name are not allowed: '${utils.getPostfixes()}'`);
|
||||
});
|
||||
|
||||
it('Configuration shouldnt be valid when duplicate friendly_name are used', async () => {
|
||||
write(configurationFile, {
|
||||
devices: {
|
||||
@@ -526,6 +539,21 @@ describe('Settings', () => {
|
||||
}).toThrowError(`friendly_name 'myname' is already in use`);
|
||||
});
|
||||
|
||||
it('Should throw when removing device which doesnt exist', async () => {
|
||||
write(configurationFile, {
|
||||
devices: {
|
||||
'0x0017880104e45519': {friendly_name: 'myname', retain: false},
|
||||
'0x0017880104e45511': {friendly_name: 'myname1', retain: false}
|
||||
},
|
||||
});
|
||||
|
||||
settings._reRead();
|
||||
|
||||
expect(() => {
|
||||
settings.removeDevice('myname33');
|
||||
}).toThrowError(`Device 'myname33' does not exist`);
|
||||
});
|
||||
|
||||
it('Shouldnt write to configuration.yaml when there are no changes in it', () => {
|
||||
const contentConfiguration = {devices: 'devices.yaml'};
|
||||
const contentDevices = {};
|
||||
|
||||
@@ -129,6 +129,24 @@ function writeDefaultConfiguration() {
|
||||
'0x90fd9ffffe4b64ae': {
|
||||
retain: false,
|
||||
friendly_name: 'tradfri_remote',
|
||||
},
|
||||
'0x90fd9ffffe4b64af': {
|
||||
friendly_name: 'roller_shutter',
|
||||
},
|
||||
'0x90fd9ffffe4b64ax': {
|
||||
friendly_name: 'ZNLDP12LM',
|
||||
},
|
||||
'0x90fd9ffffe4b64aa': {
|
||||
friendly_name: 'SP600_OLD',
|
||||
},
|
||||
'0x90fd9ffffe4b64ab': {
|
||||
friendly_name: 'SP600_NEW',
|
||||
},
|
||||
'0x90fd9ffffe4b64ac': {
|
||||
friendly_name: 'MKS-CM-W5',
|
||||
},
|
||||
'0x0017880104e45526': {
|
||||
friendly_name: 'GL-S-007ZS',
|
||||
}
|
||||
},
|
||||
groups: {
|
||||
|
||||
@@ -7,11 +7,13 @@ class Group {
|
||||
this.command = jest.fn();
|
||||
this.meta = {};
|
||||
this.members = members;
|
||||
this.removeFromDatabase = jest.fn();
|
||||
this.hasMember = (endpoint) => this.members.includes(endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
const clusters = {
|
||||
'genBasic': 0,
|
||||
'genScenes': 5,
|
||||
'genOnOff': 6,
|
||||
'genLevelCtrl': 8,
|
||||
@@ -20,8 +22,9 @@ const clusters = {
|
||||
}
|
||||
|
||||
class Endpoint {
|
||||
constructor(ID, inputClusters, outputClusters, deviceIeeeAddress, binds=[]) {
|
||||
constructor(ID, inputClusters, outputClusters, deviceIeeeAddress, binds=[], clusterValues={}) {
|
||||
this.deviceIeeeAddress = deviceIeeeAddress;
|
||||
this.clusterValues = clusterValues;
|
||||
this.ID = ID;
|
||||
this.inputClusters = inputClusters;
|
||||
this.outputClusters = outputClusters;
|
||||
@@ -33,7 +36,7 @@ class Endpoint {
|
||||
this.configureReporting = jest.fn();
|
||||
this.binds = binds;
|
||||
this.supportsInputCluster = (cluster) => {
|
||||
assert(clusters[cluster], `Undefined '${cluster}'`);
|
||||
assert(clusters[cluster] !== undefined, `Undefined '${cluster}'`);
|
||||
return this.inputClusters.includes(clusters[cluster]);
|
||||
}
|
||||
|
||||
@@ -60,13 +63,18 @@ class Endpoint {
|
||||
this.removeFromAllGroups = () => {
|
||||
Object.values(groups).forEach((g) => this.removeFromGroup(g))
|
||||
}
|
||||
|
||||
this.getClusterAttributeValue = (cluster, value) => {
|
||||
return this.clusterValues[cluster][value];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Device {
|
||||
constructor(type, ieeeAddr, networkAddress, manufacturerID, endpoints, interviewCompleted, powerSource = null, modelID = null, interviewing=false, manufacturerName) {
|
||||
constructor(type, ieeeAddr, networkAddress, manufacturerID, endpoints, interviewCompleted, powerSource = null, modelID = null, interviewing=false, manufacturerName, dateCode= null) {
|
||||
this.type = type;
|
||||
this.ieeeAddr = ieeeAddr;
|
||||
this.dateCode = dateCode;
|
||||
this.networkAddress = networkAddress;
|
||||
this.manufacturerID = manufacturerID;
|
||||
this.endpoints = endpoints;
|
||||
@@ -113,7 +121,7 @@ const devices = {
|
||||
'E1743': new Device('Router', '0x0017880104e45540', 6540,4476, [new Endpoint(1, [0], [])], true, "Mains (single phase)", 'TRADFRI on/off switch'),
|
||||
'QBKG04LM': new Device('Router', '0x0017880104e45541', 6549,4151, [new Endpoint(1, [0], []), new Endpoint(2, [0, 6], [])], true, "Mains (single phase)", 'lumi.ctrl_neutral1'),
|
||||
'QBKG03LM':new Device('Router', '0x0017880104e45542', 6540,4151, [new Endpoint(1, [0], [], '0x0017880104e45542'), new Endpoint(2, [0, 6], [], '0x0017880104e45542'), new Endpoint(3, [0, 6], [], '0x0017880104e45542')], true, "Mains (single phase)", 'lumi.ctrl_neutral2'),
|
||||
'GLEDOPTO1112': new Device('Router', '0x0017880104e45543', 6540,4151, [new Endpoint(11, [0], []), new Endpoint(13, [0], [])], true, "Mains (single phase)", 'GL-C-008'),
|
||||
'GLEDOPTO1112': new Device('Router', '0x0017880104e45543', 6540, 4151, [new Endpoint(11, [0], [], '0x0017880104e45543'), new Endpoint(13, [0], [], '0x0017880104e45543')], true, "Mains (single phase)", 'GL-C-008'),
|
||||
'GLEDOPTO111213': new Device('Router', '0x0017880104e45544', 6540,4151, [new Endpoint(11, [0], []), new Endpoint(13, [0], []), new Endpoint(12, [0], [])], true, "Mains (single phase)", 'GL-C-008'),
|
||||
'HGZB04D': new Device('Router', '0x0017880104e45545', 6540,4151, [new Endpoint(1, [0], [])], true, "Mains (single phase)", 'FB56+ZSC05HG1.0'),
|
||||
'ZNCLDJ11LM': new Device('Router', '0x0017880104e45547', 6540,4151, [new Endpoint(1, [0], []), new Endpoint(2, [0], [])], true, "Mains (single phase)", 'lumi.curtain'),
|
||||
@@ -128,6 +136,12 @@ const devices = {
|
||||
'CC2530_ROUTER': new Device('Router', '0x0017880104e45559', 6540,4151, [new Endpoint(1, [0, 6], [])], true, "Mains (single phase)", 'lumi.router'),
|
||||
'LIVOLO': new Device('Router', '0x0017880104e45560', 6541,4152, [new Endpoint(6, [0, 6], [])], true, "Mains (single phase)", 'TI0001 '),
|
||||
'tradfri_remote': new Device('EndDevice', '0x90fd9ffffe4b64ae', 33906, 4476, [new Endpoint(1, [0], [0,3,4,6,8,5], '0x90fd9ffffe4b64ae')], true, "Battery", "TRADFRI remote control"),
|
||||
'roller_shutter': new Device('EndDevice', '0x90fd9ffffe4b64af', 33906, 4476, [new Endpoint(1, [0], [0,3,4,6,8,5], '0x90fd9ffffe4b64af')], true, "Battery", "SCM-R_00.00.03.15TC"),
|
||||
'ZNLDP12LM': new Device('Router', '0x90fd9ffffe4b64ax', 33901, 4476, [new Endpoint(1, [0,4,3,5,10,258,13,19,6,1,1030,8,768,1027,1029,1026], [0,3,4,6,8,5], '0x90fd9ffffe4b64ax')], true, "Mains (single phase)", "lumi.light.aqcn02"),
|
||||
'SP600_OLD': new Device('Router', '0x90fd9ffffe4b64aa', 33901, 4476, [new Endpoint(1, [0,4,3,5,10,258,13,19,6,1,1030,8,768,1027,1029,1026], [0,3,4,6,8,5], '0x90fd9ffffe4b64aa', [], {seMetering: {"multiplier":1,"divisor":10000}})], true, "Mains (single phase)", "SP600", false, 'Salus', '20160120'),
|
||||
'SP600_NEW': new Device('Router', '0x90fd9ffffe4b64ab', 33901, 4476, [new Endpoint(1, [0,4,3,5,10,258,13,19,6,1,1030,8,768,1027,1029,1026], [0,3,4,6,8,5], '0x90fd9ffffe4b64aa', [], {seMetering: {"multiplier":1,"divisor":10000}})], true, "Mains (single phase)", "SP600", false, 'Salus', '20170220'),
|
||||
'MKS-CM-W5': new Device('Router', '0x90fd9ffffe4b64ac', 33901, 4476, [new Endpoint(1, [0,4,3,5,10,258,13,19,6,1,1030,8,768,1027,1029,1026], [0,3,4,6,8,5], '0x90fd9ffffe4b64aa', [], {})], true, "Mains (single phase)", "qnazj70", false),
|
||||
'GL-S-007ZS': new Device('Router', '0x0017880104e45526', 6540,4151, [new Endpoint(1, [0], [], '0x0017880104e45526')], true, "Mains (single phase)", 'GL-S-007ZS'),
|
||||
}
|
||||
|
||||
const groups = {
|
||||
@@ -138,6 +152,8 @@ const groups = {
|
||||
}
|
||||
|
||||
const mock = {
|
||||
setTransmitPower: jest.fn(),
|
||||
touchlinkFactoryReset: jest.fn(),
|
||||
start: jest.fn(),
|
||||
permitJoin: jest.fn(),
|
||||
getCoordinatorVersion: jest.fn().mockReturnValue({type: 'z-Stack', meta: {version: 1, revision: 20190425}}),
|
||||
|
||||
Reference in New Issue
Block a user