mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 15:18:41 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00ebd441a0 | ||
|
|
4843d85fe9 | ||
|
|
32db1fd07a | ||
|
|
cee35c66e6 | ||
|
|
76d7861671 | ||
|
|
35fd1a68fc | ||
|
|
8828ee60ae | ||
|
|
55c3edfa95 | ||
|
|
2f52b851a9 | ||
|
|
91bc56a09f | ||
|
|
c737f569f9 | ||
|
|
de4f9f44fd | ||
|
|
21714855bf | ||
|
|
50986591c1 | ||
|
|
e10999f0f2 | ||
|
|
7015741a08 | ||
|
|
64fa02abab | ||
|
|
7e6dc67861 | ||
|
|
3c5727b320 | ||
|
|
9ef4d2b810 | ||
|
|
e96dac2596 | ||
|
|
9a15e8c118 | ||
|
|
25408769e6 | ||
|
|
2328ecb2ee | ||
|
|
b2a453bec3 | ||
|
|
20d9bac317 | ||
|
|
40596bab6a | ||
|
|
69d04d0648 | ||
|
|
363f4e1c3b | ||
|
|
008fda4d90 | ||
|
|
c64158b75d | ||
|
|
1ea694cb1a | ||
|
|
e537784d34 | ||
|
|
0277591085 | ||
|
|
7b2480be52 | ||
|
|
70c30736a1 | ||
|
|
24cf72f166 | ||
|
|
e5081b2856 | ||
|
|
4048cb81a4 | ||
|
|
2af2a8ea05 | ||
|
|
1b066d9f59 | ||
|
|
5c9b31b2fc | ||
|
|
61d85af3fd | ||
|
|
789ec0cc90 | ||
|
|
b97f5d1afa | ||
|
|
4cb0c0bbb0 | ||
|
|
6d24a49062 | ||
|
|
bee722c9c4 | ||
|
|
36bba0154d | ||
|
|
a2a90bcdbd | ||
|
|
1043053812 | ||
|
|
e6044da604 | ||
|
|
a8cf925fcb | ||
|
|
ad835e090b | ||
|
|
3a96473e2b | ||
|
|
661f79ce93 | ||
|
|
7461fec28b | ||
|
|
8b4b9bea19 | ||
|
|
fdcf2e8c3d | ||
|
|
b39e195763 | ||
|
|
f2cdbeab38 | ||
|
|
f37d52b870 | ||
|
|
969585c84e | ||
|
|
d4d85633d2 | ||
|
|
f54f3bd40f | ||
|
|
03ca8ec24d | ||
|
|
a65fbd6e2c | ||
|
|
ac28ca2841 | ||
|
|
79d11e295b | ||
|
|
9f31cd86a3 | ||
|
|
c722b611dd | ||
|
|
3c4fa40a32 | ||
|
|
d81a972f1b | ||
|
|
0f9333f8b9 | ||
|
|
6e60063337 | ||
|
|
438626691e | ||
|
|
a978b26001 | ||
|
|
7cec70aba8 | ||
|
|
df1825d65e | ||
|
|
532b3b2484 | ||
|
|
84a5277d45 | ||
|
|
acbab01b7e | ||
|
|
6a4073b305 | ||
|
|
18aee4803b | ||
|
|
3b1fae1452 | ||
|
|
cf2f10394a | ||
|
|
9eed7e271e | ||
|
|
07765093be |
+4
-5
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"env": {
|
||||
"mocha": true,
|
||||
"jest/globals": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": ["eslint:recommended", "google"],
|
||||
"extends": ["eslint:recommended", "google", "plugin:jest/recommended", "plugin:jest/style"],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module"
|
||||
@@ -12,10 +12,9 @@
|
||||
"rules": {
|
||||
"require-jsdoc": "off",
|
||||
"indent": ["error", 4],
|
||||
"max-len": ["error", { "code": 120 }],
|
||||
"mocha/no-exclusive-tests": "error"
|
||||
"max-len": ["error", { "code": 120 }]
|
||||
},
|
||||
"plugins": [
|
||||
"mocha"
|
||||
"jest"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<!--
|
||||
Did you read the FAQ?
|
||||
https://www.zigbee2mqtt.io/information/FAQ.md
|
||||
https://www.zigbee2mqtt.io/information/FAQ.html
|
||||
|
||||
Unsupported device?
|
||||
https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html
|
||||
@@ -8,9 +9,22 @@ A question?
|
||||
Read the documentation: https://www.zigbee2mqtt.io/
|
||||
|
||||
Bug report:
|
||||
- First make sure your on the latest version of zigbee2mqtt.
|
||||
- First make sure you are on the latest version of zigbee2mqtt.
|
||||
- Make sure you are running the latest CC253X firmware from https://github.com/koenkk/z-stack-firmware.
|
||||
- Provide a clear description of the problem.
|
||||
- If applicable, provide steps how to reproduce the problem.
|
||||
- Provide a log with `log_level` set to `debug`, see https://www.zigbee2mqtt.io/configuration/configuration.html
|
||||
- DON'T copy logs directly here, post a link to https://hastebin.com/ or https://pastebin.com/.
|
||||
-->
|
||||
|
||||
<!--Start your bug report below this line-->
|
||||
# Bug Report
|
||||
## What happened
|
||||
|
||||
## What did you expect to happen
|
||||
|
||||
## How to reproduce it (minimal and precise)
|
||||
|
||||
## Debug Info
|
||||
zigbee2mqtt version:
|
||||
CC253X firmware version:
|
||||
|
||||
@@ -14,6 +14,23 @@ build_and_push() {
|
||||
docker push $DOCKER_USERNAME/zigbee2mqtt:$1
|
||||
}
|
||||
|
||||
push_manifest() {
|
||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
|
||||
docker manifest create $DOCKER_USERNAME/zigbee2mqtt:latest$1 $DOCKER_USERNAME/zigbee2mqtt:latest$1 $DOCKER_USERNAME/zigbee2mqtt:arm32v6$1 $DOCKER_USERNAME/zigbee2mqtt:arm64v8$1
|
||||
|
||||
docker manifest annotate $DOCKER_USERNAME/zigbee2mqtt:latest$1 $DOCKER_USERNAME/zigbee2mqtt:arm32v6$1 --os linux --arch arm --variant v6
|
||||
docker manifest annotate $DOCKER_USERNAME/zigbee2mqtt:latest$1 $DOCKER_USERNAME/zigbee2mqtt:arm64v8$1 --os linux --arch arm64 --variant v8
|
||||
|
||||
docker manifest inspect $DOCKER_USERNAME/zigbee2mqtt:latest$1
|
||||
|
||||
docker manifest push -p $DOCKER_USERNAME/zigbee2mqtt:latest$1
|
||||
|
||||
docker run --rm mplatform/mquery $DOCKER_USERNAME/zigbee2mqtt:latest$1
|
||||
|
||||
export DOCKER_CLI_EXPERIMENTAL=disabled
|
||||
}
|
||||
|
||||
# Only update Docker images for:
|
||||
# - dev branch
|
||||
# - version release
|
||||
@@ -31,6 +48,10 @@ then
|
||||
tag_push "$TRAVIS_TAG-arm32v6" "arm32v6"
|
||||
tag_push "$TRAVIS_TAG-arm64v8" "arm64v8"
|
||||
tag_push "$TRAVIS_TAG" latest
|
||||
|
||||
# Push manifest
|
||||
push_manifest ""
|
||||
|
||||
elif [ "$TRAVIS_BRANCH" = "dev" -a "$TRAVIS_PULL_REQUEST" = "false" ]
|
||||
then
|
||||
echo "Updating docker images for dev branch!"
|
||||
@@ -38,6 +59,10 @@ then
|
||||
build_and_push latest-dev docker/Dockerfile.amd64 amd64
|
||||
build_and_push arm32v6-dev docker/Dockerfile.arm32v6 arm
|
||||
build_and_push arm64v8-dev docker/Dockerfile.arm64v8 arm64
|
||||
|
||||
# Push manifest
|
||||
push_manifest "-dev"
|
||||
|
||||
else
|
||||
echo "Not updating docker images, triggered by pull request or not on master/dev branch"
|
||||
fi
|
||||
|
||||
@@ -17,6 +17,12 @@
|
||||
<a href="https://www.paypal.me/koenkk">
|
||||
<img src="https://img.shields.io/badge/donate-PayPal-blue.svg">
|
||||
</a>
|
||||
<a href="https://discord.gg/dadfWYE">
|
||||
<img src="https://img.shields.io/discord/556563650429583360.svg">
|
||||
</a>
|
||||
<a href="http://zigbee2mqtt.discourse.group/">
|
||||
<img src="https://img.shields.io/discourse/https/zigbee2mqtt.discourse.group/status.svg">
|
||||
</a>
|
||||
</div>
|
||||
<h1>Zigbee2mqtt 🌉 🐝</h1>
|
||||
<p>
|
||||
|
||||
@@ -11,6 +11,7 @@ const controller = new Controller();
|
||||
controller.start();
|
||||
|
||||
process.on('SIGINT', handleQuit);
|
||||
process.on('SIGTERM', handleQuit);
|
||||
|
||||
function handleQuit() {
|
||||
controller.stop(() => process.exit());
|
||||
|
||||
+36
-32
@@ -5,6 +5,7 @@ const logger = require('./util/logger');
|
||||
const settings = require('./util/settings');
|
||||
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
|
||||
const objectAssignDeep = require('object-assign-deep');
|
||||
const utils = require('./util/utils');
|
||||
|
||||
// Extensions
|
||||
const ExtensionNetworkMap = require('./extension/networkMap');
|
||||
@@ -26,7 +27,10 @@ class Controller {
|
||||
constructor() {
|
||||
this.zigbee = new Zigbee();
|
||||
this.mqtt = new MQTT();
|
||||
this.state = new State();
|
||||
|
||||
if (settings.get().advanced.cache_state) {
|
||||
this.state = new State();
|
||||
}
|
||||
|
||||
// Bind methods
|
||||
this.onMQTTConnected = this.onMQTTConnected.bind(this);
|
||||
@@ -130,7 +134,9 @@ class Controller {
|
||||
// Log
|
||||
logger.debug(
|
||||
`Received zigbee message of type '${message.type}' with data '${JSON.stringify(message.data)}'` +
|
||||
(device ? ` of device '${device.modelId}' (${device.ieeeAddr})` : '')
|
||||
(device ? ` of device '${device.modelId}' (${device.ieeeAddr})` : '') +
|
||||
(message.groupid ? ` with groupID ${message.groupID}` : '') +
|
||||
(message.endpoints && message.endpoints.length ? ` of endpoint ${message.endpoints[0].epId}` : '')
|
||||
);
|
||||
|
||||
// Call extensions.
|
||||
@@ -153,7 +159,9 @@ class Controller {
|
||||
}
|
||||
|
||||
start() {
|
||||
this.state.start();
|
||||
if (this.state) {
|
||||
this.state.start();
|
||||
}
|
||||
|
||||
this.startupLogVersion(() => {
|
||||
this.zigbee.start(this.onZigbeeMessage, (error) => {
|
||||
@@ -173,31 +181,17 @@ class Controller {
|
||||
this.extensions.filter((e) => e.stop).forEach((e) => e.stop());
|
||||
|
||||
// Wrap-up
|
||||
this.state.stop();
|
||||
if (this.state) {
|
||||
this.state.stop();
|
||||
}
|
||||
|
||||
this.mqtt.disconnect();
|
||||
this.zigbee.stop(callback);
|
||||
}
|
||||
|
||||
startupLogVersion(callback) {
|
||||
const git = require('git-last-commit');
|
||||
const packageJSON = require('../package.json');
|
||||
const version = packageJSON.version;
|
||||
|
||||
git.getLastCommit((err, commit) => {
|
||||
let commitHash = null;
|
||||
|
||||
if (err) {
|
||||
try {
|
||||
commitHash = require('../.hash.json').hash;
|
||||
} catch (error) {
|
||||
commitHash = 'unknown';
|
||||
}
|
||||
} else {
|
||||
commitHash = commit.shortHash;
|
||||
}
|
||||
|
||||
logger.info(`Starting zigbee2mqtt version ${version} (commit #${commitHash})`);
|
||||
|
||||
utils.getZigbee2mqttVersion((info) => {
|
||||
logger.info(`Starting zigbee2mqtt version ${info.version} (commit #${info.commitHash})`);
|
||||
callback();
|
||||
});
|
||||
}
|
||||
@@ -225,27 +219,25 @@ class Controller {
|
||||
|
||||
sendAllCachedStates() {
|
||||
this.zigbee.getAllClients().forEach((device) => {
|
||||
if (this.state.exists(device.ieeeAddr)) {
|
||||
this.publishEntityState(device.ieeeAddr, this.state.get(device.ieeeAddr), false);
|
||||
if (this.state && this.state.exists(device.ieeeAddr)) {
|
||||
this.publishEntityState(device.ieeeAddr, this.state.get(device.ieeeAddr));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
publishEntityState(entityID, payload, cache) {
|
||||
publishEntityState(entityID, payload) {
|
||||
const entity = settings.resolveEntity(entityID);
|
||||
const appSettings = settings.get();
|
||||
let messagePayload = {...payload};
|
||||
|
||||
if (appSettings.advanced.cache_state) {
|
||||
if (this.state) {
|
||||
// Add cached state to payload
|
||||
if (this.state.exists(entityID)) {
|
||||
messagePayload = objectAssignDeep.noMutate(this.state.get(entityID), payload);
|
||||
}
|
||||
|
||||
// Update state cache with new state.
|
||||
if (cache) {
|
||||
this.state.set(entityID, messagePayload);
|
||||
}
|
||||
this.state.set(entityID, messagePayload);
|
||||
}
|
||||
|
||||
const entitySettings = entity.type === 'device' ? settings.getDevice(entityID) : settings.getGroup(entityID);
|
||||
@@ -262,14 +254,23 @@ class Controller {
|
||||
this.mqtt.publish(entity.friendlyName, JSON.stringify(messagePayload), options);
|
||||
} else if (settings.get().experimental.output === 'attribute') {
|
||||
Object.keys(messagePayload).forEach((key) => {
|
||||
this.mqtt.publish(`${entity.friendlyName}/${key}`, JSON.stringify(messagePayload[key]), options);
|
||||
if (typeof messagePayload[key] == 'object') {
|
||||
Object.keys(messagePayload[key]).forEach((subKey) => {
|
||||
this.mqtt.publish(`${entity.friendlyName}/${key}-${subKey}`,
|
||||
JSON.stringify(messagePayload[key][subKey]), options);
|
||||
});
|
||||
} else {
|
||||
this.mqtt.publish(`${entity.friendlyName}/${key}`, JSON.stringify(messagePayload[key]), options);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getDeviceInfoForMqtt(ieeeAddr) {
|
||||
const device = this.zigbee.getDevice(ieeeAddr);
|
||||
const {type, nwkAddr, manufId, manufName, powerSource, modelId, status} = device;
|
||||
const {
|
||||
type, nwkAddr, manufId, manufName, powerSource, modelId, hwVersion, swBuildId, status, dateCode,
|
||||
} = device;
|
||||
const deviceSettings = settings.getDevice(device.ieeeAddr);
|
||||
|
||||
return {
|
||||
@@ -281,6 +282,9 @@ class Controller {
|
||||
manufName,
|
||||
powerSource,
|
||||
modelId,
|
||||
hwVersion: hwVersion ? hwVersion : 'unknown',
|
||||
swBuildId: swBuildId ? swBuildId : 'unknown',
|
||||
dateCode: dateCode ? dateCode : 'unknown',
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
const settings = require('../util/settings');
|
||||
const logger = require('../util/logger');
|
||||
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
|
||||
const utils = require('../util/utils');
|
||||
|
||||
const configRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/\\w+`, 'g');
|
||||
const configRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/((?:\\w+/get)|(?:\\w+))`);
|
||||
const allowedLogLevels = ['error', 'warn', 'info', 'debug'];
|
||||
|
||||
class BridgeConfig {
|
||||
@@ -15,25 +16,34 @@ class BridgeConfig {
|
||||
// Bind functions
|
||||
this.permitJoin = this.permitJoin.bind(this);
|
||||
this.lastSeen = this.lastSeen.bind(this);
|
||||
this.elapsed = this.elapsed.bind(this);
|
||||
this.reset = this.reset.bind(this);
|
||||
this.logLevel = this.logLevel.bind(this);
|
||||
this.devices = this.devices.bind(this);
|
||||
this.groups = this.groups.bind(this);
|
||||
this.rename = this.rename.bind(this);
|
||||
this.remove = this.remove.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);
|
||||
|
||||
// Set supported options
|
||||
this.supportedOptions = {
|
||||
'permit_join': this.permitJoin,
|
||||
'last_seen': this.lastSeen,
|
||||
'elapsed': this.elapsed,
|
||||
'reset': this.reset,
|
||||
'log_level': this.logLevel,
|
||||
'devices': this.devices,
|
||||
'groups': this.groups,
|
||||
'devices/get': this.devices,
|
||||
'rename': this.rename,
|
||||
'remove': this.remove,
|
||||
'ban': this.ban,
|
||||
'device_options': this.deviceOptions,
|
||||
'add_group': this.addGroup,
|
||||
'remove_group': this.removeGroup,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -88,6 +98,19 @@ class BridgeConfig {
|
||||
logger.info(`Set last_seen to ${message}`);
|
||||
}
|
||||
|
||||
elapsed(topic, message) {
|
||||
const allowed = ['true', 'false'];
|
||||
message = message.toString();
|
||||
|
||||
if (!allowed.includes(message)) {
|
||||
logger.error(`${message} is not an allowed value, possible: ${allowed}`);
|
||||
return;
|
||||
}
|
||||
|
||||
settings.set(['advanced', 'elapsed'], message === 'true');
|
||||
logger.info(`Set elapsed to ${message}`);
|
||||
}
|
||||
|
||||
logLevel(topic, message) {
|
||||
const level = message.toString().toLowerCase();
|
||||
if (allowedLogLevels.includes(level)) {
|
||||
@@ -102,19 +125,39 @@ class BridgeConfig {
|
||||
}
|
||||
|
||||
devices(topic, message) {
|
||||
const devices = this.zigbee.getAllClients().map((device) => {
|
||||
const mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
||||
const friendlyDevice = settings.getDevice(device.ieeeAddr);
|
||||
|
||||
return {
|
||||
const devices = this.zigbee.getDevices().map((device) => {
|
||||
const payload = {
|
||||
ieeeAddr: device.ieeeAddr,
|
||||
type: device.type,
|
||||
model: mappedDevice ? mappedDevice.model : device.modelId,
|
||||
friendly_name: friendlyDevice ? friendlyDevice.friendly_name : device.ieeeAddr,
|
||||
};
|
||||
|
||||
if (device.type !== 'Coordinator') {
|
||||
const mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
||||
const friendlyDevice = settings.getDevice(device.ieeeAddr);
|
||||
payload.model = mappedDevice ? mappedDevice.model : device.modelId;
|
||||
payload.friendly_name = friendlyDevice ? friendlyDevice.friendly_name : device.ieeeAddr;
|
||||
payload.nwkAddr = device.nwkAddr;
|
||||
payload.manufId = device.manufId;
|
||||
payload.manufName = device.manufName;
|
||||
payload.powerSource = device.powerSource;
|
||||
payload.modelId = device.modelId;
|
||||
payload.hwVersion = device.hwVersion;
|
||||
payload.swBuildId = device.swBuildId;
|
||||
payload.dateCode = device.dateCode;
|
||||
}
|
||||
|
||||
return payload;
|
||||
});
|
||||
|
||||
this.mqtt.log('devices', devices);
|
||||
if (topic.split('/').pop() == 'get') {
|
||||
this.mqtt.publish(`bridge/config/devices`, JSON.stringify(devices), {});
|
||||
} else {
|
||||
this.mqtt.log('devices', devices);
|
||||
}
|
||||
}
|
||||
|
||||
groups(topic, message) {
|
||||
this.mqtt.log('groups', settings.getGroups());
|
||||
}
|
||||
|
||||
rename(topic, message) {
|
||||
@@ -144,6 +187,18 @@ class BridgeConfig {
|
||||
}
|
||||
}
|
||||
|
||||
addGroup(topic, message) {
|
||||
const name = message.toString();
|
||||
const added = settings.addGroup(name);
|
||||
added ? logger.info(`Added group '${name}'`) : logger.error(`Failed to add group '${name}'`);
|
||||
}
|
||||
|
||||
removeGroup(topic, message) {
|
||||
const name = message.toString();
|
||||
const removed = settings.removeGroup(name);
|
||||
removed ? logger.info(`Removed group '${name}'`) : logger.error(`Failed to remove group '${name}'`);
|
||||
}
|
||||
|
||||
remove(topic, message) {
|
||||
this.removeOrBan(false, message);
|
||||
}
|
||||
@@ -163,7 +218,9 @@ class BridgeConfig {
|
||||
settings.removeDevice(deviceID);
|
||||
|
||||
// Remove from state
|
||||
this.state.remove(deviceID);
|
||||
if (this.state) {
|
||||
this.state.remove(deviceID);
|
||||
}
|
||||
|
||||
logger.info(`Successfully ${ban ? 'banned' : 'removed'} ${deviceID}`);
|
||||
this.mqtt.log(ban ? 'device_banned' : 'device_removed', message);
|
||||
@@ -193,7 +250,7 @@ class BridgeConfig {
|
||||
return false;
|
||||
}
|
||||
|
||||
const option = topic.split('/').slice(-1)[0];
|
||||
const option = topic.match(configRegex)[1];
|
||||
|
||||
if (!this.supportedOptions.hasOwnProperty(option)) {
|
||||
return false;
|
||||
@@ -205,13 +262,18 @@ class BridgeConfig {
|
||||
}
|
||||
|
||||
publish() {
|
||||
const topic = `bridge/config`;
|
||||
const payload = {
|
||||
log_level: logger.transports.console.level,
|
||||
permit_join: this.zigbee.getPermitJoin(),
|
||||
};
|
||||
utils.getZigbee2mqttVersion((info) => {
|
||||
const topic = `bridge/config`;
|
||||
const payload = {
|
||||
version: info.version,
|
||||
commit: info.commitHash,
|
||||
coordinator_firmware: this.zigbee.getFirmwareVersion(),
|
||||
log_level: logger.transports.console.level,
|
||||
permit_join: this.zigbee.getPermitJoin(),
|
||||
};
|
||||
|
||||
this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0}, null);
|
||||
this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0}, null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class DeviceAvailability {
|
||||
return true;
|
||||
}
|
||||
|
||||
return device.type === 'Router' && (device.powerSource && device.powerSource !== 'Battery');
|
||||
return utils.isRouter(device) && !utils.isBatteryPowered(device);
|
||||
}
|
||||
|
||||
getAllPingableDevices() {
|
||||
@@ -53,12 +53,13 @@ class DeviceAvailability {
|
||||
.forEach((device) => this.publishAvailability(device.ieeeAddr, true));
|
||||
|
||||
// Start timers for all devices
|
||||
this.getAllPingableDevices().forEach((device) => this.setTimer(device.ieeeAddr));
|
||||
this.getAllPingableDevices().forEach((device) => this.setTimer(device));
|
||||
}
|
||||
|
||||
handleInterval(ieeeAddr) {
|
||||
handleInterval(device) {
|
||||
// Check if a job is already pending.
|
||||
// This avoids overflowing of the queue in case the queue is not able to catch-up with the jobs being added.
|
||||
const ieeeAddr = device.ieeeAddr;
|
||||
if (this.pending.includes(ieeeAddr)) {
|
||||
logger.debug(`Skipping ping for ${ieeeAddr} becuase job is already in queue`);
|
||||
return;
|
||||
@@ -68,6 +69,7 @@ class DeviceAvailability {
|
||||
|
||||
// When a device is already unavailable, log the ping failed on 'debug' instead of 'error'.
|
||||
const errorLogLevel = this.state.hasOwnProperty(ieeeAddr) && !this.state[ieeeAddr] ? 'debug' : 'error';
|
||||
const mechanism = utils.isXiaomiDevice(device) ? 'basic' : 'default';
|
||||
|
||||
this.zigbee.ping(ieeeAddr, errorLogLevel, (error) => {
|
||||
this.publishAvailability(ieeeAddr, !error);
|
||||
@@ -78,8 +80,8 @@ class DeviceAvailability {
|
||||
this.pending.splice(index, 1);
|
||||
}
|
||||
|
||||
this.setTimer(ieeeAddr);
|
||||
});
|
||||
this.setTimer(device);
|
||||
}, mechanism);
|
||||
}
|
||||
|
||||
setTimer(ieeeAddr) {
|
||||
@@ -148,7 +150,7 @@ class DeviceAvailability {
|
||||
this.publishAvailability(device.ieeeAddr, true);
|
||||
}
|
||||
|
||||
this.setTimer(device.ieeeAddr);
|
||||
this.setTimer(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+42
-44
@@ -1,7 +1,8 @@
|
||||
const settings = require('../util/settings');
|
||||
const logger = require('../util/logger');
|
||||
const Queue = require('queue');
|
||||
const utils = require('../util/utils');
|
||||
|
||||
const postfixes = utils.getPostfixes();
|
||||
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/(bind|unbind)/.+$`);
|
||||
|
||||
const allowedClusters = [
|
||||
@@ -17,16 +18,25 @@ class DeviceBind {
|
||||
this.mqtt = mqtt;
|
||||
this.state = state;
|
||||
this.publishEntityState = publishEntityState;
|
||||
|
||||
// Setup queue
|
||||
this.queue = new Queue();
|
||||
this.queue.concurrency = 1;
|
||||
this.queue.autostart = true;
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/bind/+`);
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/bind/+/+`);
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/unbind/+`);
|
||||
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/unbind/+/+`);
|
||||
}
|
||||
|
||||
getIDAndPostfix(topic) {
|
||||
let postfix = null;
|
||||
if (postfixes.find((p) => topic.endsWith(`/${p}`))) {
|
||||
postfix = topic.substr(topic.lastIndexOf('/') + 1, topic.length);
|
||||
|
||||
// Remove postfix from topic
|
||||
topic = topic.replace(`/${postfix}`, '');
|
||||
}
|
||||
|
||||
return {ID: topic, postfix};
|
||||
}
|
||||
|
||||
parseTopic(topic) {
|
||||
@@ -42,8 +52,7 @@ class DeviceBind {
|
||||
|
||||
// Remove type from topic
|
||||
topic = topic.replace(`${type}/`, '');
|
||||
|
||||
return {ID: topic, type};
|
||||
return {type, ...this.getIDAndPostfix(topic)};
|
||||
}
|
||||
|
||||
onMQTTMessage(topic, message) {
|
||||
@@ -55,28 +64,21 @@ class DeviceBind {
|
||||
|
||||
// Find source; can only be a device.
|
||||
const sourceEntity = settings.resolveEntity(topic.ID);
|
||||
const source = this.zigbee.getEndpoint(sourceEntity.ID);
|
||||
const source = utils.getEndpointByEntityID(this.zigbee, sourceEntity.ID, topic.postfix);
|
||||
|
||||
if (!source) {
|
||||
logger.error(`Failed to find device '${sourceEntity.ID}'`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find target; can be a device or group.
|
||||
const targetEntity = settings.resolveEntity(message.toString());
|
||||
const targetEntityIDPostfix= this.getIDAndPostfix(message.toString());
|
||||
const targetEntity = settings.resolveEntity(targetEntityIDPostfix.ID);
|
||||
let target = null;
|
||||
|
||||
if (targetEntity.type === 'device') {
|
||||
target = this.zigbee.getEndpoint(targetEntity.ID);
|
||||
|
||||
if (!target) {
|
||||
logger.error(`Failed to find target device '${targetEntity.ID}'`);
|
||||
return false;
|
||||
}
|
||||
target = utils.getEndpointByEntityID(this.zigbee, targetEntity.ID, targetEntityIDPostfix.postfix);
|
||||
} else if (targetEntity.type === 'group') {
|
||||
target = targetEntity.ID;
|
||||
}
|
||||
|
||||
if (!source || !target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find which clusters are supported by both the source and target.
|
||||
// Groups are assumed to support all clusters (as we don't know which devices are in)
|
||||
let supported = [];
|
||||
@@ -94,29 +96,25 @@ class DeviceBind {
|
||||
|
||||
// Bind
|
||||
clusters.forEach((cluster) => {
|
||||
this.queue.push((queueCallback) => {
|
||||
logger.debug(`${topic.type}ing cluster '${cluster}' from ${sourceEntity.ID}' to '${targetEntity.ID}'`);
|
||||
logger.debug(`${topic.type}ing cluster '${cluster}' from ${sourceEntity.ID}' to '${targetEntity.ID}'`);
|
||||
|
||||
source[topic.type](cluster, target, (error) => {
|
||||
if (error) {
|
||||
logger.error(
|
||||
`Failed to ${topic.type} cluster '${cluster}' from ${sourceEntity.ID}' to ` +
|
||||
`'${targetEntity.ID}' (${error})`
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Successfully ${topic.type === 'bind' ? 'bound' : 'unbound'} cluster '${cluster}' from ` +
|
||||
`${sourceEntity.ID}' to '${targetEntity.ID}'`
|
||||
);
|
||||
this.zigbee[topic.type](source, cluster, target, (error) => {
|
||||
if (error) {
|
||||
logger.error(
|
||||
`Failed to ${topic.type} cluster '${cluster}' from ${sourceEntity.ID}' to ` +
|
||||
`'${targetEntity.ID}' (${error})`
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Successfully ${topic.type === 'bind' ? 'bound' : 'unbound'} cluster '${cluster}' from ` +
|
||||
`${sourceEntity.ID}' to '${targetEntity.ID}'`
|
||||
);
|
||||
|
||||
this.mqtt.log(
|
||||
`device_${topic.type}`,
|
||||
{from: sourceEntity.ID, to: targetEntity.ID, cluster}
|
||||
);
|
||||
}
|
||||
|
||||
queueCallback();
|
||||
});
|
||||
this.mqtt.log(
|
||||
`device_${topic.type}`,
|
||||
{from: sourceEntity.ID, to: targetEntity.ID, cluster}
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -57,11 +57,7 @@ class DeviceGroupMembership {
|
||||
}
|
||||
logger.info(`${msgGroupList} and ${msgCapacity}`);
|
||||
|
||||
// never cache
|
||||
this.publishEntityState(ieeeAddr, {
|
||||
group_list: grouplist,
|
||||
group_capacity: capacity,
|
||||
}, false);
|
||||
this.publishEntityState(ieeeAddr, {group_list: grouplist, group_capacity: capacity});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,48 +2,18 @@
|
||||
const settings = require('../util/settings');
|
||||
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
|
||||
const logger = require('../util/logger');
|
||||
const utils = require('../util/utils');
|
||||
|
||||
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/.+/(set|get)$`);
|
||||
const postfixes = [
|
||||
'left', 'right', 'center', 'bottom_left', 'bottom_right',
|
||||
'top_left', 'top_right', 'white', 'rgb', 'system', 'top', 'bottom',
|
||||
];
|
||||
const postfixes = utils.getPostfixes();
|
||||
|
||||
const maxDepth = 20;
|
||||
|
||||
const groupConverters = [
|
||||
{
|
||||
from: (converted) => {
|
||||
if (converted.cid === 'genOnOff') {
|
||||
return {state: converted.cmd.toUpperCase()};
|
||||
} else if (converted.cid === 'genLevelCtrl') {
|
||||
return {state: 'ON', brightness: Number(converted.zclData.level)};
|
||||
}
|
||||
},
|
||||
to: zigbeeShepherdConverters.toZigbeeConverters.light_onoff_brightness,
|
||||
},
|
||||
{
|
||||
from: (converted) => {
|
||||
return {color_temp: Number(converted.zclData.colortemp)};
|
||||
},
|
||||
to: zigbeeShepherdConverters.toZigbeeConverters.light_colortemp,
|
||||
},
|
||||
{
|
||||
from: (converted) => {
|
||||
if (converted.zclData.hasOwnProperty('colorx') && converted.zclData.hasOwnProperty('colory')) {
|
||||
return {
|
||||
color: {
|
||||
x: converted.zclData.colorx / 65535,
|
||||
y: converted.zclData.colory / 65535,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
to: zigbeeShepherdConverters.toZigbeeConverters.light_color,
|
||||
},
|
||||
{
|
||||
from: () => null,
|
||||
to: zigbeeShepherdConverters.toZigbeeConverters.ignore_transition,
|
||||
},
|
||||
zigbeeShepherdConverters.toZigbeeConverters.light_onoff_brightness,
|
||||
zigbeeShepherdConverters.toZigbeeConverters.light_colortemp,
|
||||
zigbeeShepherdConverters.toZigbeeConverters.light_color,
|
||||
zigbeeShepherdConverters.toZigbeeConverters.ignore_transition,
|
||||
];
|
||||
|
||||
class DevicePublish {
|
||||
@@ -70,7 +40,9 @@ class DevicePublish {
|
||||
|
||||
// Remove base from topic
|
||||
topic = topic.replace(`${settings.get().mqtt.base_topic}/`, '');
|
||||
|
||||
if (topic.match(/bridge\/config/)) {
|
||||
return null;
|
||||
}
|
||||
// Parse type from topic
|
||||
const type = topic.substr(topic.lastIndexOf('/') + 1, topic.length);
|
||||
|
||||
@@ -100,31 +72,16 @@ class DevicePublish {
|
||||
this.mqtt.log('zigbee_publish_error', error.toString(), meta);
|
||||
}
|
||||
|
||||
handlePublished(entity, topic, converter, converted) {
|
||||
if (entity.type === 'device' && topic.type === 'set') {
|
||||
// Devices do not report when they go off, this ensures state (on/off) is always in sync.
|
||||
// Brightness onoff converters also control the state. (do a moveToLevelWithOnOff)
|
||||
const msg = {};
|
||||
const _key = topic.postfix ? `state_${topic.postfix}` : 'state';
|
||||
handlePublished(entity, topic, converter, converted, key, value) {
|
||||
if (topic.type === 'set' && converted.hasOwnProperty('newState') && converted.newState) {
|
||||
const msg = converted.newState;
|
||||
|
||||
if (converted.cid === 'genOnOff') {
|
||||
msg[_key] = converted.cmd.toUpperCase();
|
||||
} else if (converted.cid === 'genLevelCtrl' && converted.cmd === 'moveToLevelWithOnOff') {
|
||||
msg[_key] = 'ON';
|
||||
msg['brightness'] = Number(converted.zclData.level);
|
||||
} else if (converted.cid === 'genLevelCtrl' && converted.cmd === 'moveToLevel') {
|
||||
msg['brightness'] = Number(converted.zclData.level);
|
||||
if (topic.postfix) {
|
||||
msg[`state_${topic.postfix}`] = msg.state;
|
||||
delete msg.state;
|
||||
}
|
||||
|
||||
if (Object.keys(msg).length > 0) {
|
||||
this.publishEntityState(entity.ID, msg, true);
|
||||
}
|
||||
} else if (entity.type === 'group' && topic.type === 'set') {
|
||||
// As a group doesn't confirm it's state, we mock the state here.
|
||||
const payload = groupConverters.find((g) => g.to === converter).from(converted);
|
||||
if (payload) {
|
||||
this.publishEntityState(entity.ID, payload, true);
|
||||
}
|
||||
this.publishEntityState(entity.ID, msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +127,7 @@ class DevicePublish {
|
||||
|
||||
converters = model.toZigbee;
|
||||
} else if (entity.type === 'group') {
|
||||
converters = groupConverters.map((g) => g.to);
|
||||
converters = groupConverters;
|
||||
}
|
||||
|
||||
// Convert the MQTT message to a Zigbee message.
|
||||
@@ -182,6 +139,23 @@ class DevicePublish {
|
||||
json = {state: message.toString()};
|
||||
}
|
||||
|
||||
/**
|
||||
* Home Assistant always publishes 'state', even when e.g. only setting
|
||||
* the color temperature. This would lead to 2 zigbee publishes, where the first one
|
||||
* (state) is probably unecessary.
|
||||
*/
|
||||
if (settings.get().homeassistant && this.state) {
|
||||
const deviceState = this.state.get(entity.ID);
|
||||
const hasColorTemp = json.hasOwnProperty('color_temp');
|
||||
const hasColor = json.hasOwnProperty('color');
|
||||
const hasBrightness = json.hasOwnProperty('brightness');
|
||||
const isOn = deviceState && deviceState.state === 'ON' ? true : false;
|
||||
if (isOn && (hasColorTemp || hasColor) && !hasBrightness) {
|
||||
delete json.state;
|
||||
logger.debug('Skipping state because of Home Assistant');
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that state and brightness are executed before other commands.
|
||||
const keys = Object.keys(json);
|
||||
keys.sort((a, b) => (['state', 'brightness'].includes(a) ? -1 : 1));
|
||||
@@ -218,7 +192,7 @@ class DevicePublish {
|
||||
endpoint,
|
||||
(error, rsp) => {
|
||||
if (!error) {
|
||||
this.handlePublished(entity, topic, converter, converted);
|
||||
this.handlePublished(entity, topic, converter, converted, key, json[key]);
|
||||
} else {
|
||||
this.handlePublishError(entity, message, error);
|
||||
}
|
||||
@@ -231,10 +205,10 @@ class DevicePublish {
|
||||
// automatically report a new state when set.
|
||||
// When reporting is requested for a device (report: true in device-specific settings) we won't
|
||||
// ever issue a read here, as we assume the device will properly report changes.
|
||||
// Only do this when the retrieve_state option is enabled for this device.
|
||||
const deviceSettings = settings.getDevice(entity.ID);
|
||||
if (topic.type === 'set' && entity.type === 'device'
|
||||
&& converted.hasOwnProperty('readAfterWriteTime')
|
||||
&& !(deviceSettings && deviceSettings.report)) {
|
||||
if (topic.type === 'set' && entity.type === 'device' && converted.hasOwnProperty('readAfterWriteTime') &&
|
||||
deviceSettings && deviceSettings.retrieve_state) {
|
||||
const getConverted = converter.convert(key, json[key], json, 'get');
|
||||
setTimeout(() => {
|
||||
this.zigbee.publish(
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
const settings = require('../util/settings');
|
||||
const logger = require('../util/logger');
|
||||
const utils = require('../util/utils');
|
||||
|
||||
const dontCacheProperties = [
|
||||
'action', 'button', 'button_left', 'button_right', 'click', 'forgotten', 'keyerror',
|
||||
'step_size', 'transition_time', 'action_color_temperature', 'action_color',
|
||||
'action_group',
|
||||
];
|
||||
const debounce = require('debounce');
|
||||
|
||||
/**
|
||||
* This extensions handles messages received from devices.
|
||||
@@ -19,12 +14,28 @@ class DeviceReceive {
|
||||
this.publishEntityState = publishEntityState;
|
||||
this.coordinator = null;
|
||||
this.elapsed = {};
|
||||
this.debouncers = {};
|
||||
}
|
||||
|
||||
onZigbeeStarted() {
|
||||
this.coordinator = this.zigbee.getCoordinator().device.ieeeAddr;
|
||||
}
|
||||
|
||||
publishDebounce(ieeeAddr, payload, time) {
|
||||
if (!this.debouncers[ieeeAddr]) {
|
||||
this.debouncers[ieeeAddr] = {
|
||||
payload: {},
|
||||
publish: debounce(() => {
|
||||
this.publishEntityState(ieeeAddr, this.debouncers[ieeeAddr].payload);
|
||||
this.debouncers[ieeeAddr].payload = {};
|
||||
}, time * 1000),
|
||||
};
|
||||
}
|
||||
|
||||
this.debouncers[ieeeAddr].payload = {...this.debouncers[ieeeAddr].payload, ...payload};
|
||||
this.debouncers[ieeeAddr].publish();
|
||||
}
|
||||
|
||||
onZigbeeMessage(message, device, mappedDevice) {
|
||||
if (message.type == 'devInterview') {
|
||||
if (!settings.getDevice(message.data)) {
|
||||
@@ -51,7 +62,8 @@ class DeviceReceive {
|
||||
}
|
||||
|
||||
// Check if this is a new device.
|
||||
if (!settings.getDevice(device.ieeeAddr)) {
|
||||
const settingsDevice = settings.getDevice(device.ieeeAddr);
|
||||
if (!settingsDevice) {
|
||||
logger.info(`New device '${device.modelId}' with address ${device.ieeeAddr} connected!`);
|
||||
settings.addDevice(device.ieeeAddr);
|
||||
this.mqtt.log('device_connected', device.ieeeAddr, {modelID: device.modelId});
|
||||
@@ -68,6 +80,25 @@ class DeviceReceive {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't handle re-transmitted Xiaomi messages.
|
||||
* https://github.com/Koenkk/zigbee2mqtt/issues/1238
|
||||
*
|
||||
* Some Xiaomi router devices re-transmit messages from Xiaomi end devices.
|
||||
* The source address of these message is set to the one of the Xiaomi router.
|
||||
* Therefore it looks like if the message came from the Xiaomi router, while in
|
||||
* fact it came from the end device.
|
||||
* Handling these message would result in false state updates.
|
||||
* The group ID attribute of these message defines the source address of the end device.
|
||||
* As the same message is also received directly from the end device, it makes no sense
|
||||
* to handle these messages.
|
||||
*/
|
||||
const hasGroupID = message.hasOwnProperty('groupid') && message.groupid != 0;
|
||||
if (utils.isXiaomiDevice(device) && utils.isRouter(device) && hasGroupID) {
|
||||
logger.debug('Skipping re-transmitted Xiaomi message');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find a conveter for this message.
|
||||
const cid = message.data.cid;
|
||||
const cmdId = message.data.cmdId;
|
||||
@@ -75,8 +106,15 @@ class DeviceReceive {
|
||||
if (cid) {
|
||||
// readRsp messages have the same structure as attReport messages.
|
||||
// search for attReport converters on readRsp.
|
||||
const search = message.type === 'readRsp' ? 'attReport' : message.type;
|
||||
return c.cid === cid && c.type === search;
|
||||
if (c.cid === cid) {
|
||||
if (c.type instanceof Array) {
|
||||
return c.type.includes(message.type);
|
||||
} else {
|
||||
return c.type === message.type;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} else if (cmdId) {
|
||||
return c.cmd === cmdId;
|
||||
}
|
||||
@@ -113,14 +151,6 @@ class DeviceReceive {
|
||||
// - If NO payload is returned do nothing. This is for non-standard behaviour
|
||||
// for e.g. click switches where we need to count number of clicks and detect long presses.
|
||||
const publish = (payload) => {
|
||||
// Don't cache messages with following properties:
|
||||
let cache = true;
|
||||
dontCacheProperties.forEach((property) => {
|
||||
if (payload.hasOwnProperty(property)) {
|
||||
cache = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Add device linkquality.
|
||||
if (message.hasOwnProperty('linkquality')) {
|
||||
payload.linkquality = message.linkquality;
|
||||
@@ -148,7 +178,12 @@ class DeviceReceive {
|
||||
this.elapsed[device.ieeeAddr] = now;
|
||||
}
|
||||
|
||||
this.publishEntityState(device.ieeeAddr, payload, cache);
|
||||
// Check if we have to debounce
|
||||
if (settingsDevice && settingsDevice.hasOwnProperty('debounce')) {
|
||||
this.publishDebounce(device.ieeeAddr, payload, settingsDevice.debounce);
|
||||
} else {
|
||||
this.publishEntityState(device.ieeeAddr, payload);
|
||||
}
|
||||
};
|
||||
|
||||
let payload = {};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
|
||||
const logger = require('../util/logger');
|
||||
const CC2530Router = zigbeeShepherdConverters.devices.find((d) => d.model === 'CC2530.ROUTER');
|
||||
|
||||
const candidates = {
|
||||
'genOnOff': {
|
||||
@@ -21,7 +22,7 @@ const reportInterval = {
|
||||
max: 3600,
|
||||
};
|
||||
|
||||
const reportableChange = 0;
|
||||
const reportableChange = 1;
|
||||
|
||||
class DeviceReport {
|
||||
constructor(zigbee, mqtt, state, publishEntityState) {
|
||||
@@ -34,6 +35,11 @@ class DeviceReport {
|
||||
setupReporting(mappedDevice, device) {
|
||||
let epId = null;
|
||||
|
||||
if (mappedDevice === CC2530Router) {
|
||||
logger.debug('Not setting up reporting for CC2530 router');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this device uses a different epId.
|
||||
if (mappedDevice.hasOwnProperty('ep')) {
|
||||
const eps = mappedDevice.ep(device);
|
||||
@@ -48,12 +54,15 @@ class DeviceReport {
|
||||
}
|
||||
|
||||
logger.debug(`Setting up reporting for ${device.ieeeAddr}`);
|
||||
|
||||
Object.values(endpoint.clusters).filter((c) => c).forEach((c) => {
|
||||
const cluster = c.attrs.cid;
|
||||
if (candidates[cluster]) {
|
||||
const candidate = candidates[cluster];
|
||||
const attributeNames = candidate.attrs.filter((a) => c.attrs.hasOwnProperty(a));
|
||||
let attributeNames = candidate.attrs.filter((a) => c.attrs.hasOwnProperty(a));
|
||||
|
||||
// Sometimes a cluster has no attributes, in this case setup reporting for all attributes.
|
||||
attributeNames = attributeNames.length ? attributeNames : candidate.attrs;
|
||||
|
||||
const attributes = attributeNames.map((attribute) => {
|
||||
return {
|
||||
attr: attribute,
|
||||
|
||||
@@ -65,6 +65,16 @@ const configurations = {
|
||||
device_class: 'gas',
|
||||
},
|
||||
},
|
||||
'binary_sensor_carbon_monoxide': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'carbon_monoxide',
|
||||
discovery_payload: {
|
||||
payload_on: true,
|
||||
payload_off: false,
|
||||
value_template: '{{ value_json.carbon_monoxide }}',
|
||||
device_class: 'safety',
|
||||
},
|
||||
},
|
||||
'binary_sensor_router': {
|
||||
type: 'binary_sensor',
|
||||
object_id: 'router',
|
||||
@@ -309,8 +319,8 @@ const mapping = {
|
||||
'WXKG12LM': [configurations.sensor_click, configurations.sensor_battery],
|
||||
'WXKG03LM': [configurations.sensor_click, configurations.sensor_battery],
|
||||
'WXKG02LM': [configurations.sensor_click, configurations.sensor_battery],
|
||||
'QBKG04LM': [configurations.switch],
|
||||
'QBKG03LM': [switchWithPostfix('left'), switchWithPostfix('right')],
|
||||
'QBKG04LM': [configurations.switch, configurations.sensor_click],
|
||||
'QBKG03LM': [switchWithPostfix('left'), switchWithPostfix('right'), configurations.sensor_click],
|
||||
'WSDCGQ01LM': [configurations.sensor_temperature, configurations.sensor_humidity, configurations.sensor_battery],
|
||||
'WSDCGQ11LM': [
|
||||
configurations.sensor_temperature, configurations.sensor_humidity, configurations.sensor_pressure,
|
||||
@@ -379,8 +389,11 @@ const mapping = {
|
||||
'SL 110 M': [configurations.light_brightness],
|
||||
'SL 110 W': [configurations.light_brightness],
|
||||
'AA68199': [configurations.light_brightness_colortemp],
|
||||
'QBKG11LM': [configurations.switch, configurations.sensor_power],
|
||||
'QBKG12LM': [switchWithPostfix('left'), switchWithPostfix('right'), configurations.sensor_power],
|
||||
'QBKG11LM': [configurations.switch, configurations.sensor_power, configurations.sensor_click],
|
||||
'QBKG12LM': [
|
||||
switchWithPostfix('left'), switchWithPostfix('right'), configurations.sensor_power,
|
||||
configurations.sensor_click,
|
||||
],
|
||||
'K2RGBW01': [configurations.light_brightness_colortemp_colorxy],
|
||||
'9290011370': [configurations.light_brightness],
|
||||
'DNCKATSW001': [configurations.switch],
|
||||
@@ -500,7 +513,7 @@ const mapping = {
|
||||
'AV2010/32': [],
|
||||
'HGZB-07A': [configurations.light_brightness_colortemp_colorxy],
|
||||
'E1524': [configurations.sensor_action],
|
||||
'GL-C-006': [configurations.light_brightness_colortemp],
|
||||
'GL-C-006/GL-C-009': [configurations.light_brightness_colortemp],
|
||||
'100.424.11': [configurations.light_brightness_colortemp],
|
||||
'AC0251100NJ': [configurations.sensor_click, configurations.sensor_brightness, configurations.sensor_battery],
|
||||
'71831': [configurations.light_brightness_colortemp],
|
||||
@@ -574,6 +587,54 @@ const mapping = {
|
||||
configurations.sensor_temperature, configurations.binary_sensor_contact,
|
||||
configurations.sensor_battery, configurations.binary_sensor_battery_low,
|
||||
],
|
||||
'IM6001-BTP01': [configurations.sensor_click, configurations.sensor_temperature],
|
||||
'AV2010/34': [configurations.sensor_click],
|
||||
'PP-WHT-US': [configurations.switch, configurations.sensor_power],
|
||||
'CR701-YZ': [
|
||||
configurations.binary_sensor_battery_low, configurations.binary_sensor_carbon_monoxide,
|
||||
configurations.binary_sensor_gas,
|
||||
],
|
||||
'HGZB-1S': [configurations.switch, configurations.sensor_click],
|
||||
'HGZB-045': [configurations.switch, configurations.sensor_click],
|
||||
'HGZB-43': [switchWithPostfix('top'), switchWithPostfix('bottom'), switchWithPostfix('center')],
|
||||
'HGZB-01A/02A': [configurations.switch],
|
||||
'MCT-350 SMA': [configurations.binary_sensor_contact],
|
||||
'3310-S': [configurations.sensor_temperature, configurations.sensor_battery],
|
||||
'3315-S': [
|
||||
configurations.sensor_temperature, configurations.binary_sensor_water_leak,
|
||||
configurations.sensor_battery,
|
||||
],
|
||||
'F-MLT-US-2': [
|
||||
configurations.sensor_temperature, configurations.binary_sensor_contact,
|
||||
configurations.sensor_battery, configurations.binary_sensor_battery_low,
|
||||
],
|
||||
'SWO-KEF1PA': [configurations.sensor_action],
|
||||
'HGZB-02S': [configurations.sensor_click, configurations.switch],
|
||||
'HGZB-41': [configurations.switch],
|
||||
'ZG9101SAC-HP': [configurations.light_brightness],
|
||||
'RS 122': [configurations.light_brightness],
|
||||
'GL-B-001Z': [configurations.light_brightness_colortemp_colorxy],
|
||||
'IM6001-MTP01': [
|
||||
configurations.sensor_temperature, configurations.sensor_battery,
|
||||
configurations.binary_sensor_occupancy,
|
||||
],
|
||||
'U86K31ND6': [switchWithPostfix('left'), switchWithPostfix('right'), switchWithPostfix('center')],
|
||||
'HLD812-Z-SC': [configurations.light_brightness],
|
||||
'BY 285 C': [configurations.light_brightness_colortemp_colorxy],
|
||||
'HS1RC-M': [configurations.sensor_action, configurations.sensor_battery],
|
||||
'SWO-WDS1PA': [configurations.binary_sensor_contact],
|
||||
'LLKZMK11LM': [switchWithPostfix('l1'), switchWithPostfix('l2')],
|
||||
'LVS-SM10ZW': [configurations.binary_sensor_contact, configurations.binary_sensor_battery_low],
|
||||
'HS2SK': [configurations.switch, configurations.sensor_power],
|
||||
'45853GE': [configurations.switch],
|
||||
'50064': [configurations.light_brightness_colortemp],
|
||||
'9290011998B': [configurations.light_brightness_colortemp],
|
||||
'4096730U7': [configurations.light_brightness_colortemp],
|
||||
'RB 278 T': [configurations.light_brightness],
|
||||
'3315-G': [
|
||||
configurations.sensor_temperature, configurations.sensor_battery, configurations.binary_sensor_water_leak,
|
||||
],
|
||||
'N2G-SP': [configurations.sensor_power, configurations.switch],
|
||||
};
|
||||
|
||||
Object.keys(mapping).forEach((key) => {
|
||||
@@ -601,6 +662,8 @@ class HomeAssistant {
|
||||
if (settings.get().experimental.output === 'attribute') {
|
||||
throw new Error('Home Assitant integration is not possible with attribute output!');
|
||||
}
|
||||
|
||||
this.discoveryTopic = settings.get().advanced.homeassistant_discovery_topic;
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
@@ -705,6 +768,10 @@ class HomeAssistant {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if (['number', 'string'].includes(typeof obj[key])) {
|
||||
payload[key] = obj[key];
|
||||
} else if (key === 'device' && typeof obj[key] === 'object') {
|
||||
Object.keys(obj['device']).forEach((key) => {
|
||||
payload['device'][key] = obj['device'][key];
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -716,7 +783,7 @@ class HomeAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0}, null, 'homeassistant');
|
||||
this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0}, null, this.discoveryTopic);
|
||||
});
|
||||
|
||||
this.discovered[entityID] = true;
|
||||
@@ -731,8 +798,8 @@ class HomeAssistant {
|
||||
const timer = setTimeout(() => {
|
||||
// Publish all device states.
|
||||
this.zigbee.getAllClients().forEach((device) => {
|
||||
if (this.state.exists(device.ieeeAddr)) {
|
||||
this.publishEntityState(device.ieeeAddr, this.state.get(device.ieeeAddr), false);
|
||||
if (this.state && this.state.exists(device.ieeeAddr)) {
|
||||
this.publishEntityState(device.ieeeAddr, this.state.get(device.ieeeAddr));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ class Livolo {
|
||||
}
|
||||
|
||||
_sendToggle(zdev, ieeeAddr, ep, retry) {
|
||||
this.zigbee.queue.push((queueCallback) => {
|
||||
this.zigbee.queue.push(ieeeAddr, (queueCallback) => {
|
||||
const cfg = {};
|
||||
logger.debug(`LIVOLO ${ieeeAddr}. Sending the 'toggle' command. Retry: ${retry}`);
|
||||
ep.functional('genOnOff', 'toggle', [cfg], foundationCfg,
|
||||
@@ -118,7 +118,7 @@ class Livolo {
|
||||
}
|
||||
|
||||
_sendPoll(zdev, ieeeAddr, ep, retry) {
|
||||
this.zigbee.queue.push((queueCallback) => {
|
||||
this.zigbee.queue.push(ieeeAddr, (queueCallback) => {
|
||||
ep.foundation('genOnOff', 'read', [{
|
||||
attrId: 0, // onOff
|
||||
}], this._handleCommandRespWithData.bind({
|
||||
|
||||
+13
-6
@@ -1,5 +1,7 @@
|
||||
const utils = require('../util/utils');
|
||||
const interval = utils.secondsToMilliseconds(60);
|
||||
const logger = require('../util/logger');
|
||||
const settings = require('../util/settings');
|
||||
|
||||
/**
|
||||
* This extensions handles Xiaomi devices.
|
||||
@@ -28,7 +30,14 @@ class Xiaomi {
|
||||
}
|
||||
});
|
||||
|
||||
this.startTimer();
|
||||
/**
|
||||
* Device availability already pings Xiaomi devices.
|
||||
*/
|
||||
if (!settings.get().advanced.availability_timeout) {
|
||||
this.startTimer();
|
||||
} else {
|
||||
logger.debug('Disabled pinging of Xiaomi devices in Xiaomi extension because availability is enabled');
|
||||
}
|
||||
}
|
||||
|
||||
startTimer() {
|
||||
@@ -48,15 +57,13 @@ class Xiaomi {
|
||||
}
|
||||
|
||||
ping(ieeeAddr) {
|
||||
this.zigbee.ping(ieeeAddr);
|
||||
this.zigbee.ping(ieeeAddr, 'error', null, 'basic');
|
||||
}
|
||||
|
||||
handleInterval() {
|
||||
this.zigbee.getAllClients()
|
||||
.filter((d) => utils.isXiaomiDevice(d)) // Filter Xiaomi devices
|
||||
.filter((d) => d.type === 'Router') // Filter routers
|
||||
.filter((d) => d.powerSource && d.powerSource !== 'Battery') // Remove battery powered devices
|
||||
.forEach((d) => this.ping(d.ieeeAddr)); // Ping devices.
|
||||
.filter((d) => utils.isXiaomiDevice(d) && utils.isRouter(d) && !utils.isBatteryPowered(d))
|
||||
.forEach((d) => this.ping(d.ieeeAddr));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+15
-1
@@ -2,9 +2,16 @@ const logger = require('./util/logger');
|
||||
const data = require('./util/data');
|
||||
const fs = require('fs');
|
||||
const settings = require('./util/settings');
|
||||
const objectAssignDeep = require('object-assign-deep');
|
||||
|
||||
const saveInterval = 1000 * 60 * 5; // 5 minutes
|
||||
|
||||
const dontCacheProperties = [
|
||||
'action', 'button', 'button_left', 'button_right', 'click', 'forgotten', 'keyerror',
|
||||
'step_size', 'transition_time', 'action_color_temperature', 'action_color',
|
||||
'action_group', 'group_list', 'group_capacity', 'no_occupancy_since',
|
||||
];
|
||||
|
||||
class State {
|
||||
constructor() {
|
||||
this.state = {};
|
||||
@@ -83,7 +90,14 @@ class State {
|
||||
}
|
||||
|
||||
set(ieeeAddr, state) {
|
||||
this.state[ieeeAddr] = state;
|
||||
const s = objectAssignDeep.noMutate(state);
|
||||
dontCacheProperties.forEach((property) => {
|
||||
if (s.hasOwnProperty(property)) {
|
||||
delete s[property];
|
||||
}
|
||||
});
|
||||
|
||||
this.state[ieeeAddr] = s;
|
||||
}
|
||||
|
||||
remove(ieeeAddr) {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
const yaml = require('js-yaml');
|
||||
const fs = require('fs');
|
||||
|
||||
function readYaml(file) {
|
||||
return yaml.safeLoad(fs.readFileSync(file, 'utf8'));
|
||||
}
|
||||
|
||||
function readYamlIfExists(file) {
|
||||
return fs.existsSync(file) ? readYaml(file) : null;
|
||||
}
|
||||
|
||||
function writeYaml(file, content) {
|
||||
fs.writeFileSync(file, yaml.safeDump(content));
|
||||
}
|
||||
|
||||
module.exports = {readYaml, readYamlIfExists, writeYaml};
|
||||
+75
-20
@@ -1,13 +1,12 @@
|
||||
const yaml = require('js-yaml');
|
||||
const fs = require('fs');
|
||||
const data = require('./data');
|
||||
const file = data.joinPath('configuration.yaml');
|
||||
const objectAssignDeep = require(`object-assign-deep`);
|
||||
const path = require('path');
|
||||
|
||||
const fs = require('./fs');
|
||||
const onChangeHandlers = [];
|
||||
|
||||
const defaults = {
|
||||
ban: [],
|
||||
permit_join: false,
|
||||
mqtt: {
|
||||
include_device_information: false,
|
||||
@@ -67,12 +66,27 @@ const defaults = {
|
||||
* Enables reporting feature
|
||||
*/
|
||||
report: false,
|
||||
|
||||
/**
|
||||
* Home Assistant discovery topic
|
||||
*/
|
||||
homeassistant_discovery_topic: 'homeassistant',
|
||||
},
|
||||
};
|
||||
|
||||
let _settings;
|
||||
|
||||
const getSettings = () => _settings||(_settings = read());
|
||||
function getSettings() {
|
||||
if (!_settings) {
|
||||
_settings = read();
|
||||
}
|
||||
|
||||
if (_settings.hasOwnProperty('advanced') && !_settings.advanced) {
|
||||
delete _settings.advanced;
|
||||
}
|
||||
|
||||
return _settings;
|
||||
}
|
||||
|
||||
function writeRead() {
|
||||
write();
|
||||
@@ -84,41 +98,33 @@ function write() {
|
||||
const settings = getSettings();
|
||||
const toWrite = objectAssignDeep.noMutate(settings);
|
||||
|
||||
// Read settings to check if we have to split devices/groups into seperate file.
|
||||
const actual = readYaml(file);
|
||||
// Read settings to check if we have to split devices/groups into separate file.
|
||||
const actual = fs.readYaml(file);
|
||||
if (typeof actual.devices === 'string') {
|
||||
writeYaml(data.joinPath(actual.devices), settings.devices);
|
||||
fs.writeYaml(data.joinPath(actual.devices), settings.devices);
|
||||
toWrite.devices = actual.devices;
|
||||
}
|
||||
|
||||
if (typeof actual.groups === 'string') {
|
||||
writeYaml(data.joinPath(actual.groups), settings.groups);
|
||||
fs.writeYaml(data.joinPath(actual.groups), settings.groups);
|
||||
toWrite.groups = actual.groups;
|
||||
}
|
||||
|
||||
writeYaml(file, toWrite);
|
||||
}
|
||||
|
||||
function readYaml(file) {
|
||||
return yaml.safeLoad(fs.readFileSync(file, 'utf8'));
|
||||
}
|
||||
|
||||
function writeYaml(file, content) {
|
||||
fs.writeFileSync(file, yaml.safeDump(content));
|
||||
fs.writeYaml(file, toWrite);
|
||||
}
|
||||
|
||||
function read() {
|
||||
const s = readYaml(file);
|
||||
const s = fs.readYaml(file);
|
||||
|
||||
// Read devices/groups configuration from separate file.
|
||||
if (typeof s.devices === 'string') {
|
||||
const file = data.joinPath(s.devices);
|
||||
s.devices = fs.existsSync(file) ? readYaml(file) : null;
|
||||
s.devices = fs.readYamlIfExists(file);
|
||||
}
|
||||
|
||||
if (typeof s.groups === 'string') {
|
||||
const file = data.joinPath(s.groups);
|
||||
s.groups = fs.existsSync(file) ? readYaml(file) : null;
|
||||
s.groups = fs.readYamlIfExists(file);
|
||||
}
|
||||
|
||||
return s;
|
||||
@@ -162,6 +168,17 @@ function addDevice(ieeeAddr) {
|
||||
writeRead();
|
||||
}
|
||||
|
||||
function banDevice(ieeeAddr) {
|
||||
const settings = getSettings();
|
||||
if (!settings.ban) {
|
||||
settings.ban = [];
|
||||
}
|
||||
|
||||
settings.ban.push(ieeeAddr);
|
||||
|
||||
writeRead();
|
||||
}
|
||||
|
||||
function removeDevice(ieeeAddr) {
|
||||
const settings = getSettings();
|
||||
if (!settings.devices || !settings.devices[ieeeAddr]) return;
|
||||
@@ -170,6 +187,40 @@ function removeDevice(ieeeAddr) {
|
||||
writeRead();
|
||||
}
|
||||
|
||||
function addGroup(groupName) {
|
||||
const settings = getSettings();
|
||||
if (!settings.groups) {
|
||||
settings.groups = {};
|
||||
}
|
||||
|
||||
let ID = '1';
|
||||
while (settings.groups.hasOwnProperty(ID)) {
|
||||
ID = (Number.parseInt(ID) + 1).toString();
|
||||
}
|
||||
|
||||
settings.groups[ID] = {friendly_name: groupName};
|
||||
writeRead();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function removeGroup(name) {
|
||||
const settings = getSettings();
|
||||
if (!settings.groups) return;
|
||||
|
||||
const ID = Object.keys(settings.groups).find((key) => {
|
||||
return settings.groups[key].friendly_name === name;
|
||||
});
|
||||
|
||||
if (ID) {
|
||||
delete settings.groups[ID];
|
||||
writeRead();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getIeeeAddrByFriendlyName(friendlyName) {
|
||||
const entry = Object.entries(getDevices()).find(([ieeeAddr, device]) =>
|
||||
device.friendly_name === friendlyName
|
||||
@@ -250,9 +301,13 @@ module.exports = {
|
||||
|
||||
getDevice,
|
||||
getGroup,
|
||||
getGroups,
|
||||
getDevices,
|
||||
banDevice: (ieeeAddr) => banDevice(ieeeAddr),
|
||||
addDevice: (ieeeAddr) => addDevice(ieeeAddr),
|
||||
removeDevice: (ieeeAddr) => removeDevice(ieeeAddr),
|
||||
addGroup: (name) => addGroup(name),
|
||||
removeGroup: (name) => removeGroup(name),
|
||||
|
||||
getIeeeAddrByFriendlyName: (friendlyName) => getIeeeAddrByFriendlyName(friendlyName),
|
||||
getGroupIDByFriendlyName: (friendlyName) => getGroupIDByFriendlyName(friendlyName),
|
||||
|
||||
+71
-1
@@ -1,3 +1,6 @@
|
||||
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
|
||||
const logger = require('../util/logger');
|
||||
|
||||
// Xiaomi uses 4151 and 4447 (lumi.plug) as manufacturer ID.
|
||||
const xiaomiManufacturerID = [4151, 4447];
|
||||
const ikeaTradfriManufacturerID = [4476];
|
||||
@@ -24,11 +27,78 @@ function toLocalISOString(dDate) {
|
||||
':' + pad(tzOffset % 60);
|
||||
}
|
||||
|
||||
const postfixes = [
|
||||
'left', 'right', 'center', 'bottom_left', 'bottom_right',
|
||||
'top_left', 'top_right', 'white', 'rgb', 'system', 'top', 'bottom',
|
||||
];
|
||||
|
||||
function getEndpointByEntityID(zigbee, entityID, epName) {
|
||||
const device = zigbee.getDevice(entityID);
|
||||
if (!device) {
|
||||
logger.error(`Failed to find device with entity ID '${entityID}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
const mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
||||
if (!mappedDevice) {
|
||||
logger.error(`Device with model ID ${device.modelId} is not supported`);
|
||||
return;
|
||||
}
|
||||
|
||||
let epID = null;
|
||||
if (epName) {
|
||||
if (!mappedDevice.ep) {
|
||||
logger.error(`Device ${mappedDevice.model} doesn't define eps`);
|
||||
return;
|
||||
}
|
||||
|
||||
epID = mappedDevice.ep(device)[epName];
|
||||
if (!epID) {
|
||||
logger.error(`Device ${mappedDevice.model} doesn't have ep named '${epName}'`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const endpoint = zigbee.getEndpoint(entityID, epID);
|
||||
if (!endpoint) {
|
||||
logger.error(`Failed to retrieve for entity ID ${entityID} and endpoint ID ${epID}`);
|
||||
}
|
||||
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
function getZigbee2mqttVersion(callback) {
|
||||
const git = require('git-last-commit');
|
||||
const packageJSON = require('../../package.json');
|
||||
const version = packageJSON.version;
|
||||
|
||||
git.getLastCommit((err, commit) => {
|
||||
let commitHash = null;
|
||||
|
||||
if (err) {
|
||||
try {
|
||||
commitHash = require('../.hash.json').hash;
|
||||
} catch (error) {
|
||||
commitHash = 'unknown';
|
||||
}
|
||||
} else {
|
||||
commitHash = commit.shortHash;
|
||||
}
|
||||
|
||||
callback({commitHash, version});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
millisecondsToSeconds: (milliseconds) => milliseconds / 1000,
|
||||
secondsToMilliseconds: (seconds) => seconds * 1000,
|
||||
isXiaomiDevice: (device) => xiaomiManufacturerID.includes(device.manufId),
|
||||
isXiaomiDevice: (device) => device.modelId !== 'lumi.router' && xiaomiManufacturerID.includes(device.manufId),
|
||||
isIkeaTradfriDevice: (device) => ikeaTradfriManufacturerID.includes(device.manufId),
|
||||
isRouter: (device) => device.type === 'Router',
|
||||
isBatteryPowered: (device) => device.powerSource && device.powerSource === 'Battery',
|
||||
isNumeric: (string) => /^\d+$/.test(string),
|
||||
toLocalISOString: (dDate) => toLocalISOString(dDate),
|
||||
getPostfixes: () => postfixes,
|
||||
getEndpointByEntityID,
|
||||
getZigbee2mqttVersion,
|
||||
};
|
||||
|
||||
+102
-27
@@ -68,13 +68,13 @@ class Zigbee {
|
||||
);
|
||||
callback(error);
|
||||
} else {
|
||||
this.logStartupInfo();
|
||||
this._handleStarted();
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
}, utils.secondsToMilliseconds(60));
|
||||
} else {
|
||||
this.logStartupInfo();
|
||||
this._handleStarted();
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
@@ -83,14 +83,45 @@ class Zigbee {
|
||||
this.shepherd.on('ready', this.onReady);
|
||||
this.shepherd.on('ind', this.onMessage);
|
||||
this.shepherd.on('error', this.onError);
|
||||
this._acceptDevIncoming = this._acceptDevIncoming.bind(this);
|
||||
this.shepherd.acceptDevIncoming = this._acceptDevIncoming;
|
||||
}
|
||||
|
||||
_handleStarted() {
|
||||
this.logStartupInfo();
|
||||
|
||||
this.getAllClients().forEach((device) => {
|
||||
if (settings.get().ban.includes(device.ieeeAddr)) {
|
||||
logger.warn(`Banned device is connected (${device.ieeeAddr}), removing..`);
|
||||
this.removeDevice(device.ieeeAddr, false, () => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_acceptDevIncoming(devInfo, callback) {
|
||||
logger.debug(
|
||||
`Accept device incoming with ieeeAddr '${devInfo.ieeeAddr}' permit join is '${this.getPermitJoin()}'`
|
||||
);
|
||||
|
||||
if (settings.get().ban.includes(devInfo.ieeeAddr)) {
|
||||
logger.info(`Banned device tried to connect (${devInfo.ieeeAddr})`);
|
||||
callback(null, false);
|
||||
} else {
|
||||
logger.debug(`Allowing device '${devInfo.ieeeAddr}' to join`);
|
||||
callback(null, true);
|
||||
}
|
||||
}
|
||||
|
||||
logStartupInfo() {
|
||||
logger.info('zigbee-shepherd started');
|
||||
logger.info(`Coordinator firmware version: '${this.shepherd.info().firmware.revision}'`);
|
||||
logger.info(`Coordinator firmware version: '${this.getFirmwareVersion()}'`);
|
||||
logger.debug(`zigbee-shepherd info: ${JSON.stringify(this.shepherd.info())}`);
|
||||
}
|
||||
|
||||
getFirmwareVersion() {
|
||||
return this.shepherd.info().firmware.revision;
|
||||
}
|
||||
|
||||
softReset(callback) {
|
||||
this.shepherd.reset('soft', callback);
|
||||
}
|
||||
@@ -159,11 +190,16 @@ class Zigbee {
|
||||
}
|
||||
|
||||
removeDevice(deviceID, ban, callback) {
|
||||
this.shepherd.remove(deviceID, {reJoin: !ban}, (error) => {
|
||||
if (ban) {
|
||||
settings.banDevice(deviceID);
|
||||
}
|
||||
|
||||
this.shepherd.remove(deviceID, {reJoin: true}, (error) => {
|
||||
if (error) {
|
||||
logger.warn(`Failed to remove '${deviceID}', trying force remove...`);
|
||||
this.forceRemove(deviceID, callback);
|
||||
} else {
|
||||
logger.info(`Removed ${deviceID}`);
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
@@ -173,7 +209,10 @@ class Zigbee {
|
||||
const device = this.shepherd._findDevByAddr(deviceID);
|
||||
|
||||
if (device) {
|
||||
return this.shepherd._unregisterDev(device, (error) => callback(error));
|
||||
return this.shepherd._unregisterDev(device, (error) => {
|
||||
logger.info(`Force removed ${deviceID}`);
|
||||
callback(error);
|
||||
});
|
||||
} else {
|
||||
logger.warn(`Could not find ${deviceID} for force removal`);
|
||||
callback(true);
|
||||
@@ -271,41 +310,77 @@ class Zigbee {
|
||||
});
|
||||
}
|
||||
|
||||
ping(ieeeAddr, errorLogLevel='error', cb) {
|
||||
const device = this.shepherd._findDevByAddr(ieeeAddr);
|
||||
ping(ieeeAddr, errorLogLevel='error', cb, mechanism='default') {
|
||||
const callback = (error) => {
|
||||
if (error) {
|
||||
logger[errorLogLevel](`Failed to ping ${ieeeAddr}`);
|
||||
} else {
|
||||
logger.debug(`Successfully pinged ${ieeeAddr}`);
|
||||
}
|
||||
|
||||
if (device) {
|
||||
this.queue.push(ieeeAddr, (queueCallback) => {
|
||||
logger.debug(`Ping ${ieeeAddr}`);
|
||||
this.shepherd.controller.checkOnline(device, (error) => {
|
||||
if (error) {
|
||||
logger[errorLogLevel](`Failed to ping ${ieeeAddr}`);
|
||||
} else {
|
||||
logger.debug(`Successfully pinged ${ieeeAddr}`);
|
||||
}
|
||||
if (cb) {
|
||||
cb(error);
|
||||
}
|
||||
};
|
||||
|
||||
if (cb) {
|
||||
cb(error);
|
||||
}
|
||||
|
||||
queueCallback(error);
|
||||
if (mechanism === 'default') {
|
||||
const device = this.shepherd._findDevByAddr(ieeeAddr);
|
||||
if (device) {
|
||||
logger.debug(`Ping ${ieeeAddr} (default)`);
|
||||
this.queue.push(ieeeAddr, (queueCallback) => {
|
||||
this.shepherd.controller.checkOnline(device, (error) => {
|
||||
callback(error);
|
||||
queueCallback(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
} else if (mechanism === 'basic') {
|
||||
const endpoint = this.getEndpoint(ieeeAddr, null);
|
||||
if (endpoint) {
|
||||
logger.debug(`Ping ${ieeeAddr} (basic)`);
|
||||
this.queue.push(ieeeAddr, (queueCallback) => {
|
||||
endpoint.foundation('genBasic', 'read', [{attrId: 0}], (error) => {
|
||||
callback(error);
|
||||
queueCallback(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bind(ep, cluster, target=this.getCoordinator()) {
|
||||
const log = `for ${ep.device.ieeeAddr} - ${cluster}`;
|
||||
bind(ep, cluster, target, callback) {
|
||||
const log = ` ${ep.device.ieeeAddr} - ${cluster}`;
|
||||
target = !target ? this.getCoordinator() : target;
|
||||
|
||||
this.queue.push(ep.device.ieeeAddr, (queueCallback) => {
|
||||
logger.debug(`Setup binding ${log}`);
|
||||
logger.debug(`Binding ${log}`);
|
||||
ep.bind(cluster, target, (error) => {
|
||||
if (error) {
|
||||
logger.error(`Failed to setup binding ${log} - (${error})`);
|
||||
logger.error(`Failed to bind ${log} - (${error})`);
|
||||
} else {
|
||||
logger.debug(`Successfully setup binding ${log}`);
|
||||
logger.debug(`Successfully bound ${log}`);
|
||||
}
|
||||
|
||||
callback(error);
|
||||
queueCallback(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
unbind(ep, cluster, target, callback) {
|
||||
const log = ` ${ep.device.ieeeAddr} - ${cluster}`;
|
||||
target = !target ? this.getCoordinator() : target;
|
||||
|
||||
this.queue.push(ep.device.ieeeAddr, (queueCallback) => {
|
||||
logger.debug(`Unbinding ${log}`);
|
||||
ep.unbind(cluster, target, (error) => {
|
||||
if (error) {
|
||||
logger.error(`Failed to unbind ${log} - (${error})`);
|
||||
} else {
|
||||
logger.debug(`Successfully unbound ${log}`);
|
||||
}
|
||||
|
||||
callback(error);
|
||||
queueCallback(error);
|
||||
});
|
||||
});
|
||||
|
||||
Generated
+2983
-1997
File diff suppressed because it is too large
Load Diff
+10
-14
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zigbee2mqtt",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.0",
|
||||
"description": "Zigbee to MQTT bridge using zigbee-shepherd",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
@@ -20,11 +20,10 @@
|
||||
"cc2531"
|
||||
],
|
||||
"scripts": {
|
||||
"coverage": "nyc --all report --reporter=text mocha --recursive --timeout=3000",
|
||||
"docgen": "node docgen/docgen.js",
|
||||
"coverage": "jest --collect-coverage",
|
||||
"eslint": "node_modules/.bin/eslint .",
|
||||
"start": "node index.js",
|
||||
"test": "mocha --recursive"
|
||||
"test": "jest"
|
||||
},
|
||||
"author": "Koen Kanters",
|
||||
"license": "GPL-3.0",
|
||||
@@ -33,6 +32,7 @@
|
||||
},
|
||||
"homepage": "https://koenkk.github.io/zigbee2mqtt",
|
||||
"dependencies": {
|
||||
"debounce": "*",
|
||||
"git-last-commit": "*",
|
||||
"js-yaml": "*",
|
||||
"mkdir-recursive": "*",
|
||||
@@ -43,21 +43,17 @@
|
||||
"rimraf": "*",
|
||||
"semver": "*",
|
||||
"winston": "2.4.2",
|
||||
"zcl-id": "git+https://github.com/Koenkk/zcl-id.git#77ba3058c6c4b09a9958761ae5e3046765c12872",
|
||||
"zcl-id": "git+https://github.com/Koenkk/zcl-id.git#4fa75c92424cd070a3bf6e1f4640b1e3ad8f802d",
|
||||
"ziee": "*",
|
||||
"zigbee-shepherd": "git+https://github.com/Koenkk/zigbee-shepherd.git#f1f3701c20a125c939504e46c82ac4dda692ff41",
|
||||
"zigbee-shepherd-converters": "7.2.7",
|
||||
"zigbee-shepherd": "git+https://github.com/Koenkk/zigbee-shepherd.git#12c2e7f875e8ebccc3971736a4b684f86de9d79e",
|
||||
"zigbee-shepherd-converters": "8.1.1",
|
||||
"zive": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "*",
|
||||
"chai-spies": "*",
|
||||
"@types/jest": "^24.0.11",
|
||||
"eslint": "*",
|
||||
"eslint-config-google": "*",
|
||||
"eslint-plugin-mocha": "*",
|
||||
"mocha": "*",
|
||||
"nyc": "*",
|
||||
"sinon": "*",
|
||||
"tmp": "*"
|
||||
"eslint-plugin-jest": "^22.3.0",
|
||||
"jest": "^24.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
const BridgeConfig = require('../lib/extension/bridgeConfig');
|
||||
const settings = require('../lib/util/settings');
|
||||
const fs = require('../lib/util/fs');
|
||||
const objectAssignDeep = require('object-assign-deep');
|
||||
const data = require('../lib/util/data');
|
||||
const configurationFile = data.joinPath('configuration.yaml');
|
||||
|
||||
const mqtt = {
|
||||
subscribe: (topic) => {},
|
||||
log: (type, message) => {},
|
||||
};
|
||||
|
||||
describe('BridgeConfig', () => {
|
||||
let bridgeConfig;
|
||||
const write = (file, json) => fs.writeYaml(file, json);
|
||||
const read = (file) => fs.readYaml(file);
|
||||
const files = new Map();
|
||||
|
||||
beforeAll(() => {
|
||||
jest.spyOn(fs, 'readYaml').mockImplementation((file) => {
|
||||
if (files.has(file)) return objectAssignDeep.noMutate(files.get(file));
|
||||
throw new Error(`Fake file not found: ${file}`);
|
||||
});
|
||||
jest.spyOn(fs, 'readYamlIfExists').mockImplementation((file) => {
|
||||
if (files.has(file)) return objectAssignDeep.noMutate(files.get(file));
|
||||
return null;
|
||||
});
|
||||
jest.spyOn(fs, 'writeYaml').mockImplementation((file, content) => {
|
||||
files.set(file, objectAssignDeep.noMutate(content));
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
settings._clear();
|
||||
files.clear();
|
||||
bridgeConfig = new BridgeConfig(null, mqtt, null, null);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
fs.readYaml.mockRestore();
|
||||
fs.readYamlIfExists.mockRestore();
|
||||
fs.writeYaml.mockRestore();
|
||||
});
|
||||
|
||||
it('Setting elapsed false', async () => {
|
||||
write(configurationFile, {advanced: {elapsed: true}});
|
||||
bridgeConfig.onMQTTMessage('zigbee2mqtt/bridge/config/elapsed', 'false');
|
||||
const expected = {
|
||||
advanced: {
|
||||
elapsed: false,
|
||||
},
|
||||
};
|
||||
|
||||
expect(read(configurationFile)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Add groups when there are none', async () => {
|
||||
write(configurationFile, {});
|
||||
bridgeConfig.onMQTTMessage('zigbee2mqtt/bridge/config/add_group', 'test123');
|
||||
const expected = {
|
||||
groups: {
|
||||
'1': {
|
||||
friendly_name: 'test123',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(read(configurationFile)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Add groups when there are', async () => {
|
||||
write(configurationFile, {
|
||||
groups: {
|
||||
'1': {
|
||||
friendly_name: 'test123',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bridgeConfig.onMQTTMessage('zigbee2mqtt/bridge/config/add_group', 'test1234');
|
||||
const expected = {
|
||||
groups: {
|
||||
'1': {
|
||||
friendly_name: 'test123',
|
||||
},
|
||||
'2': {
|
||||
friendly_name: 'test1234',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(read(configurationFile)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Remove group', async () => {
|
||||
write(configurationFile, {
|
||||
groups: {
|
||||
'1': {
|
||||
friendly_name: 'test123',
|
||||
},
|
||||
'2': {
|
||||
friendly_name: 'test1234',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bridgeConfig.onMQTTMessage('zigbee2mqtt/bridge/config/remove_group', 'test123');
|
||||
const expected = {
|
||||
groups: {
|
||||
'2': {
|
||||
friendly_name: 'test1234',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(read(configurationFile)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Get groups', async () => {
|
||||
jest.spyOn(mqtt, 'log').mockImplementation((type, message) => {
|
||||
expect(type).toBe('groups');
|
||||
expect(Object.keys(message)).toHaveLength(2);
|
||||
expect(message['1'].friendly_name).toBe('test1');
|
||||
expect(message['4711'].friendly_name).toBe('test42');
|
||||
});
|
||||
|
||||
write(configurationFile, {
|
||||
groups: {
|
||||
'1': {
|
||||
friendly_name: 'test1',
|
||||
},
|
||||
'4711': {
|
||||
friendly_name: 'test42',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
bridgeConfig.onMQTTMessage('zigbee2mqtt/bridge/config/groups', 'whatever');
|
||||
});
|
||||
});
|
||||
+53
-54
@@ -1,5 +1,3 @@
|
||||
const assert = require('chai').assert;
|
||||
const sinon = require('sinon');
|
||||
const Controller = require('../lib/controller');
|
||||
const settings = require('../lib/util/settings');
|
||||
const mqtt = require('../lib/mqtt');
|
||||
@@ -10,24 +8,25 @@ describe('Controller', () => {
|
||||
let mqttPublish;
|
||||
|
||||
beforeEach(() => {
|
||||
utils.stubLogger(sinon);
|
||||
sinon.stub(settings, 'getDevice').callsFake((ieeeAddr) => {
|
||||
return {friendly_name: 'test'};
|
||||
});
|
||||
mqttPublish = sinon.stub(mqtt.prototype, 'publish').callsFake(() => {});
|
||||
utils.stubLogger(jest);
|
||||
jest.spyOn(settings, 'getDevice').mockReturnValue({friendly_name: 'test'});
|
||||
mqttPublish = jest.spyOn(mqtt.prototype, 'publish').mockReturnValue(undefined);
|
||||
controller = new Controller();
|
||||
controller.zigbee = {
|
||||
getDevice: () => {
|
||||
return {
|
||||
modelId: 'TRADFRI bulb E27 CWS opal 600lm',
|
||||
manufName: 'IKEA',
|
||||
hwVersion: '1.1',
|
||||
swBuildId: '2.0',
|
||||
dateCode: '2018-01-01',
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Handling zigbee messages', () => {
|
||||
@@ -35,71 +34,71 @@ describe('Controller', () => {
|
||||
const device = {ieeeAddr: '0x12345678', modelId: 'TRADFRI bulb E27 CWS opal 600lm'};
|
||||
const message = utils.zigbeeMessage(device, 'genOnOff', 'devChange', {onOff: 1}, 1);
|
||||
controller.onZigbeeMessage(message);
|
||||
assert.isTrue(mqttPublish.calledOnce);
|
||||
assert.strictEqual(
|
||||
mqttPublish.getCall(0).args[1],
|
||||
JSON.stringify({state: 'ON'})
|
||||
);
|
||||
expect(mqttPublish).toHaveBeenCalledTimes(1);
|
||||
expect(mqttPublish.mock.calls[0][1]).toBe(JSON.stringify({state: 'ON'}));
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message when include_device_information is set', () => {
|
||||
sinon.stub(settings, 'get').callsFake(() => {
|
||||
return {
|
||||
mqtt: {
|
||||
include_device_information: true,
|
||||
},
|
||||
advanced: {
|
||||
cache_state: false,
|
||||
},
|
||||
experimental: {
|
||||
output: 'json',
|
||||
},
|
||||
};
|
||||
jest.spyOn(settings, 'get').mockReturnValue({
|
||||
mqtt: {
|
||||
include_device_information: true,
|
||||
},
|
||||
advanced: {
|
||||
cache_state: false,
|
||||
},
|
||||
experimental: {
|
||||
output: 'json',
|
||||
},
|
||||
});
|
||||
|
||||
const payload = {
|
||||
'state': 'ON',
|
||||
'device': {
|
||||
'ieeeAddr': '0x12345678',
|
||||
'friendlyName': 'test',
|
||||
'manufName': 'IKEA',
|
||||
'modelId': 'TRADFRI bulb E27 CWS opal 600lm',
|
||||
'hwVersion': '1.1',
|
||||
'swBuildId': '2.0',
|
||||
'dateCode': '2018-01-01',
|
||||
},
|
||||
};
|
||||
|
||||
const device = {ieeeAddr: '0x12345678', modelId: 'TRADFRI bulb E27 CWS opal 600lm'};
|
||||
const message = utils.zigbeeMessage(device, 'genOnOff', 'devChange', {onOff: 1}, 1);
|
||||
controller.onZigbeeMessage(message);
|
||||
assert.isTrue(mqttPublish.calledOnce);
|
||||
assert.strictEqual(
|
||||
mqttPublish.getCall(0).args[1],
|
||||
`{"state":"ON","device":{"ieeeAddr":"0x12345678","friendlyName":"test",` +
|
||||
`"manufName":"IKEA","modelId":"TRADFRI bulb E27 CWS opal 600lm"}}`
|
||||
);
|
||||
});
|
||||
expect(mqttPublish).toHaveBeenCalledTimes(1);
|
||||
expect(JSON.parse(mqttPublish.mock.calls[0][1])).toStrictEqual(payload);
|
||||
}
|
||||
);
|
||||
|
||||
it('Should output to json by default', () => {
|
||||
const payload = {temperature: 1, humidity: 2};
|
||||
controller.publishEntityState('0x12345678', payload);
|
||||
assert.isTrue(mqttPublish.calledOnce);
|
||||
assert.deepEqual(
|
||||
JSON.parse(mqttPublish.getCall(0).args[1]),
|
||||
payload
|
||||
);
|
||||
expect(mqttPublish).toHaveBeenCalledTimes(1);
|
||||
expect(JSON.parse(mqttPublish.mock.calls[0][1])).toStrictEqual(payload);
|
||||
});
|
||||
|
||||
it('Should output to attribute', () => {
|
||||
sinon.stub(settings, 'get').callsFake(() => {
|
||||
return {
|
||||
mqtt: {
|
||||
include_device_information: false,
|
||||
},
|
||||
advanced: {
|
||||
cache_state: false,
|
||||
},
|
||||
experimental: {
|
||||
output: 'attribute',
|
||||
},
|
||||
};
|
||||
jest.spyOn(settings, 'get').mockReturnValue({
|
||||
mqtt: {
|
||||
include_device_information: false,
|
||||
},
|
||||
advanced: {
|
||||
cache_state: false,
|
||||
},
|
||||
experimental: {
|
||||
output: 'attribute',
|
||||
},
|
||||
});
|
||||
|
||||
const payload = {temperature: 1, humidity: 2};
|
||||
controller.publishEntityState('0x12345678', payload);
|
||||
assert.isTrue(mqttPublish.calledTwice);
|
||||
assert.deepEqual(mqttPublish.getCall(0).args[0], 'test/temperature');
|
||||
assert.deepEqual(mqttPublish.getCall(0).args[1], '1');
|
||||
assert.deepEqual(mqttPublish.getCall(1).args[0], 'test/humidity');
|
||||
assert.deepEqual(mqttPublish.getCall(1).args[1], '2');
|
||||
expect(mqttPublish).toHaveBeenCalledTimes(2);
|
||||
expect(mqttPublish.mock.calls[0][0]).toBe('test/temperature');
|
||||
expect(mqttPublish.mock.calls[0][1]).toBe('1');
|
||||
expect(mqttPublish.mock.calls[1][0]).toBe('test/humidity');
|
||||
expect(mqttPublish.mock.calls[1][1]).toBe('2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+2
-3
@@ -1,4 +1,3 @@
|
||||
const assert = require('chai').assert;
|
||||
const data = require('../lib/util/data.js');
|
||||
const path = require('path');
|
||||
|
||||
@@ -7,7 +6,7 @@ describe('Data', () => {
|
||||
it('Should return correct path', () => {
|
||||
const expected = path.normalize(path.join(__dirname, '..', 'data'));
|
||||
const actual = data.getPath();
|
||||
assert.strictEqual(actual, expected);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('Should return correct path when ZIGBEE2MQTT_DATA set', () => {
|
||||
@@ -15,7 +14,7 @@ describe('Data', () => {
|
||||
process.env.ZIGBEE2MQTT_DATA = expected;
|
||||
data._reload();
|
||||
const actual = data.getPath();
|
||||
assert.strictEqual(actual, expected);
|
||||
expect(actual).toBe(expected);
|
||||
delete process.env.ZIGBEE2MQTT_DATA;
|
||||
data._reload();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
const assert = require('chai').assert;
|
||||
const sinon = require('sinon');
|
||||
const Availability = require('../lib/extension/deviceAvailability');
|
||||
const utils = require('./utils');
|
||||
|
||||
@@ -7,12 +5,12 @@ describe('Availability', () => {
|
||||
let availability;
|
||||
|
||||
beforeEach(() => {
|
||||
utils.stubLogger(sinon);
|
||||
utils.stubLogger(jest);
|
||||
availability = new Availability(null, null, null, () => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Determine pingable devices', () => {
|
||||
@@ -22,7 +20,7 @@ describe('Availability', () => {
|
||||
type: 'Router',
|
||||
};
|
||||
|
||||
assert.isTrue(availability.isPingable(device));
|
||||
expect(availability.isPingable(device)).toBe(true);
|
||||
});
|
||||
|
||||
it('Battery device should not be a pingable device', () => {
|
||||
@@ -31,7 +29,7 @@ describe('Availability', () => {
|
||||
type: 'EndDevice',
|
||||
};
|
||||
|
||||
assert.isFalse(availability.isPingable(device));
|
||||
expect(availability.isPingable(device)).toBe(false);
|
||||
});
|
||||
|
||||
it('E11-G13 should be a pingable device', () => {
|
||||
@@ -42,7 +40,7 @@ describe('Availability', () => {
|
||||
manufId: 4448,
|
||||
};
|
||||
|
||||
assert.isTrue(availability.isPingable(device));
|
||||
expect(availability.isPingable(device)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
const DeviceBind = require('../lib/extension/deviceBind');
|
||||
const utils = require('./utils');
|
||||
const settings = require('../lib/util/settings');
|
||||
|
||||
const mqtt = {
|
||||
subscribe: () => {},
|
||||
log: () => {},
|
||||
};
|
||||
|
||||
const devices = {
|
||||
bulb: {
|
||||
getSimpleDesc: () => {
|
||||
return {inClusterList: [6, 8]};
|
||||
},
|
||||
},
|
||||
remote: {
|
||||
getSimpleDesc: () => {
|
||||
return {outClusterList: [5, 6, 8]};
|
||||
},
|
||||
},
|
||||
switch_ep2: {
|
||||
getSimpleDesc: () => {
|
||||
return {outClusterList: [5, 6]};
|
||||
},
|
||||
},
|
||||
switch_ep3: {
|
||||
getSimpleDesc: () => {
|
||||
return {inClusterList: [5, 6]};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const zigbee = {
|
||||
bind: jest.fn((ep, cluster, target, callback) => {
|
||||
callback(false, null);
|
||||
}),
|
||||
unbind: jest.fn((ep, cluster, target, callback) => {
|
||||
callback(false, null);
|
||||
}),
|
||||
getEndpoint: (ID, ep) => {
|
||||
if (ID === 'bulb' || ID === '0x002') {
|
||||
return devices.bulb;
|
||||
} else if (ID === 'remote' || ID === '0x001') {
|
||||
return devices.remote;
|
||||
} else if (ep == 2 && ID === 'switch_ep2') {
|
||||
return devices.switch_ep2;
|
||||
} else if (ep == 3 && ID === 'switch_ep3') {
|
||||
return devices.switch_ep3;
|
||||
}
|
||||
|
||||
throw new Error(`No mock for ${ID} and ep ${ep}`);
|
||||
},
|
||||
getDevice: (ID) => {
|
||||
const lookup = {
|
||||
'switch_ep2': 'lumi.sensor_86sw2.es1',
|
||||
'switch_ep3': 'DNCKAT_S003',
|
||||
'bulb': 'TRADFRI bulb E27 WS opal 980lm',
|
||||
'remote': 'TRADFRI remote control',
|
||||
'0x002': 'TRADFRI bulb E27 WS opal 980lm',
|
||||
'0x001': 'TRADFRI remote control',
|
||||
};
|
||||
|
||||
if (lookup.hasOwnProperty(ID)) {
|
||||
return {modelId: lookup[ID]};
|
||||
}
|
||||
|
||||
throw new Error(`No mock for ${ID}`);
|
||||
},
|
||||
};
|
||||
|
||||
describe('DeviceBind', () => {
|
||||
let deviceBind;
|
||||
|
||||
beforeEach(() => {
|
||||
utils.stubLogger(jest);
|
||||
deviceBind = new DeviceBind(zigbee, mqtt, null, null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
zigbee.bind.mockClear();
|
||||
zigbee.unbind.mockClear();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Bind devices', () => {
|
||||
it('Bind', async () => {
|
||||
deviceBind.onMQTTMessage('zigbee2mqtt/bridge/bind/remote', 'bulb');
|
||||
expect(zigbee.bind).toHaveBeenCalledTimes(2);
|
||||
expect(zigbee.bind).toHaveBeenNthCalledWith(1,
|
||||
devices.remote,
|
||||
6,
|
||||
devices.bulb,
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(zigbee.bind).toHaveBeenNthCalledWith(2,
|
||||
devices.remote,
|
||||
8,
|
||||
devices.bulb,
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Bind by friendly name', async () => {
|
||||
jest.spyOn(settings, 'getIeeeAddrByFriendlyName').mockImplementation((friendlyName) => {
|
||||
const lookup = {
|
||||
remote: '0x001',
|
||||
bulb: '0x002',
|
||||
};
|
||||
|
||||
return lookup[friendlyName];
|
||||
});
|
||||
|
||||
deviceBind.onMQTTMessage('zigbee2mqtt/bridge/bind/remote', 'bulb');
|
||||
|
||||
expect(zigbee.bind).toHaveBeenCalledTimes(2);
|
||||
expect(zigbee.bind).toHaveBeenNthCalledWith(1,
|
||||
devices.remote,
|
||||
6,
|
||||
devices.bulb,
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(zigbee.bind).toHaveBeenNthCalledWith(2,
|
||||
devices.remote,
|
||||
8,
|
||||
devices.bulb,
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Bind non default ep', async () => {
|
||||
deviceBind.onMQTTMessage('zigbee2mqtt/bridge/bind/switch_ep2/right', 'bulb');
|
||||
expect(zigbee.bind).toHaveBeenCalledTimes(1);
|
||||
expect(zigbee.bind).toHaveBeenNthCalledWith(1,
|
||||
devices.switch_ep2,
|
||||
6,
|
||||
devices.bulb,
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Bind non default ep to target with non default ep', async () => {
|
||||
deviceBind.onMQTTMessage('zigbee2mqtt/bridge/bind/switch_ep2/right', 'switch_ep3/right');
|
||||
expect(zigbee.bind).toHaveBeenCalledTimes(2);
|
||||
expect(zigbee.bind).toHaveBeenNthCalledWith(1,
|
||||
devices.switch_ep2,
|
||||
5,
|
||||
devices.switch_ep3,
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(zigbee.bind).toHaveBeenNthCalledWith(2,
|
||||
devices.switch_ep2,
|
||||
6,
|
||||
devices.switch_ep3,
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Unbind devices', () => {
|
||||
it('Unbind', async () => {
|
||||
deviceBind.onMQTTMessage('zigbee2mqtt/bridge/unbind/remote', 'bulb');
|
||||
expect(zigbee.unbind).toHaveBeenCalledTimes(2);
|
||||
expect(zigbee.unbind).toHaveBeenNthCalledWith(1,
|
||||
devices.remote,
|
||||
6,
|
||||
devices.bulb,
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(zigbee.unbind).toHaveBeenNthCalledWith(2,
|
||||
devices.remote,
|
||||
8,
|
||||
devices.bulb,
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+919
-481
File diff suppressed because it is too large
Load Diff
+115
-90
@@ -1,15 +1,16 @@
|
||||
const assert = require('chai').assert;
|
||||
const sinon = require('sinon');
|
||||
const DeviceReceive = require('../lib/extension/deviceReceive');
|
||||
const settings = require('../lib/util/settings');
|
||||
const devices = require('zigbee-shepherd-converters').devices;
|
||||
const utils = require('./utils');
|
||||
|
||||
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
// Devices
|
||||
const WXKG11LM = devices.find((d) => d.model === 'WXKG11LM');
|
||||
const WXKG02LM = devices.find((d) => d.model === 'WXKG02LM');
|
||||
const WSDCGQ11LM = devices.find((d) => d.model === 'WSDCGQ11LM');
|
||||
const RTCGQ11LM = devices.find((d) => d.model === 'RTCGQ11LM');
|
||||
const ZNCZ02LM = devices.find((d) => d.model === 'ZNCZ02LM');
|
||||
|
||||
const mqtt = {
|
||||
log: () => {},
|
||||
@@ -20,14 +21,14 @@ describe('DeviceReceive', () => {
|
||||
let publishEntityState;
|
||||
|
||||
beforeEach(() => {
|
||||
utils.stubLogger(sinon);
|
||||
sinon.stub(settings, 'addDevice').callsFake(() => {});
|
||||
publishEntityState = sinon.spy();
|
||||
utils.stubLogger(jest);
|
||||
jest.spyOn(settings, 'addDevice').mockReturnValue(undefined);
|
||||
publishEntityState = jest.fn();
|
||||
deviceReceive = new DeviceReceive(null, mqtt, null, publishEntityState);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Handling zigbee messages', () => {
|
||||
@@ -35,24 +36,24 @@ describe('DeviceReceive', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 1);
|
||||
deviceReceive.onZigbeeMessage(message, device, WXKG11LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {click: 'single'});
|
||||
expect(publishEntityState).toHaveBeenCalledTimes(1);
|
||||
expect(publishEntityState.mock.calls[0][1]).toStrictEqual({click: 'single'});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message which uses ep (left)', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 1);
|
||||
deviceReceive.onZigbeeMessage(message, device, WXKG02LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {click: 'left'});
|
||||
expect(publishEntityState).toHaveBeenCalledTimes(1);
|
||||
expect(publishEntityState.mock.calls[0][1]).toStrictEqual({click: 'left'});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message which uses ep (right)', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 2);
|
||||
deviceReceive.onZigbeeMessage(message, device, WXKG02LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {click: 'right'});
|
||||
expect(publishEntityState).toHaveBeenCalledTimes(1);
|
||||
expect(publishEntityState.mock.calls[0][1]).toStrictEqual({click: 'right'});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with default precision', () => {
|
||||
@@ -61,118 +62,133 @@ describe('DeviceReceive', () => {
|
||||
device, 'msTemperatureMeasurement', 'attReport', {measuredValue: -85}, 1
|
||||
);
|
||||
deviceReceive.onZigbeeMessage(message, device, WSDCGQ11LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {temperature: -0.85});
|
||||
expect(publishEntityState).toHaveBeenCalledTimes(1);
|
||||
expect(publishEntityState.mock.calls[0][1]).toStrictEqual({temperature: -0.85});
|
||||
});
|
||||
|
||||
it('Should debounce messages', async () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
jest.spyOn(settings, 'getDevice').mockReturnValue({debounce: 0.1});
|
||||
const message1 = utils.zigbeeMessage(
|
||||
device, 'msTemperatureMeasurement', 'attReport', {measuredValue: 8}, 1
|
||||
);
|
||||
const message2 = utils.zigbeeMessage(
|
||||
device, 'msRelativeHumidity', 'attReport', {measuredValue: 1}, 1
|
||||
);
|
||||
const message3 = utils.zigbeeMessage(
|
||||
device, 'msPressureMeasurement', 'attReport', {measuredValue: 2}, 1
|
||||
);
|
||||
deviceReceive.onZigbeeMessage(message1, device, WSDCGQ11LM);
|
||||
deviceReceive.onZigbeeMessage(message2, device, WSDCGQ11LM);
|
||||
deviceReceive.onZigbeeMessage(message3, device, WSDCGQ11LM);
|
||||
await wait(200);
|
||||
expect(publishEntityState).toHaveBeenCalledTimes(1);
|
||||
expect(publishEntityState.mock.calls[0][1]).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2});
|
||||
|
||||
deviceReceive.onZigbeeMessage(message1, device, WSDCGQ11LM);
|
||||
await wait(200);
|
||||
expect(publishEntityState).toHaveBeenCalledTimes(2);
|
||||
expect(publishEntityState.mock.calls[1][1]).toStrictEqual({temperature: 0.08});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with 1 precision', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
sinon.stub(settings, 'getDevice').callsFake(() => {
|
||||
return {temperature_precision: 1};
|
||||
});
|
||||
jest.spyOn(settings, 'getDevice').mockReturnValue({temperature_precision: 1});
|
||||
const message = utils.zigbeeMessage(
|
||||
device, 'msTemperatureMeasurement', 'attReport', {measuredValue: -85}, 1
|
||||
);
|
||||
deviceReceive.onZigbeeMessage(message, device, WSDCGQ11LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {temperature: -0.8});
|
||||
expect(publishEntityState).toHaveBeenCalledTimes(1);
|
||||
expect(publishEntityState.mock.calls[0][1]).toStrictEqual({temperature: -0.8});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with 0 precision', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
sinon.stub(settings, 'getDevice').callsFake(() => {
|
||||
return {temperature_precision: 0};
|
||||
});
|
||||
jest.spyOn(settings, 'getDevice').mockReturnValue({temperature_precision: 0});
|
||||
const message = utils.zigbeeMessage(
|
||||
device, 'msTemperatureMeasurement', 'attReport', {measuredValue: -85}, 1
|
||||
);
|
||||
deviceReceive.onZigbeeMessage(message, device, WSDCGQ11LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {temperature: -1});
|
||||
expect(publishEntityState).toHaveBeenCalledTimes(1);
|
||||
expect(publishEntityState.mock.calls[0][1]).toStrictEqual({temperature: -1});
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with 1 precision when set via device_options', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
sinon.stub(settings, 'get').callsFake(() => {
|
||||
return {
|
||||
device_options: {
|
||||
temperature_precision: 1,
|
||||
},
|
||||
advanced: {
|
||||
last_seen: 'disable',
|
||||
},
|
||||
};
|
||||
});
|
||||
sinon.stub(settings, 'getDevice').callsFake(() => {
|
||||
return {};
|
||||
jest.spyOn(settings, 'get').mockReturnValue({
|
||||
device_options: {
|
||||
temperature_precision: 1,
|
||||
},
|
||||
advanced: {
|
||||
last_seen: 'disable',
|
||||
},
|
||||
});
|
||||
jest.spyOn(settings, 'getDevice').mockReturnValue({});
|
||||
const message = utils.zigbeeMessage(
|
||||
device, 'msTemperatureMeasurement', 'attReport', {measuredValue: -85}, 1
|
||||
);
|
||||
deviceReceive.onZigbeeMessage(message, device, WSDCGQ11LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {temperature: -0.8});
|
||||
});
|
||||
expect(publishEntityState).toHaveBeenCalledTimes(1);
|
||||
expect(publishEntityState.mock.calls[0][1]).toStrictEqual({temperature: -0.8});
|
||||
}
|
||||
);
|
||||
|
||||
it('Should handle a zigbee message with 2 precision when overrides device_options', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
sinon.stub(settings, 'get').callsFake(() => {
|
||||
return {
|
||||
device_options: {
|
||||
temperature_precision: 1,
|
||||
},
|
||||
advanced: {
|
||||
last_seen: 'disable',
|
||||
},
|
||||
};
|
||||
jest.spyOn(settings, 'get').mockReturnValue({
|
||||
device_options: {
|
||||
temperature_precision: 1,
|
||||
},
|
||||
advanced: {
|
||||
last_seen: 'disable',
|
||||
},
|
||||
});
|
||||
sinon.stub(settings, 'getDevice').callsFake(() => {
|
||||
return {
|
||||
temperature_precision: 2,
|
||||
};
|
||||
jest.spyOn(settings, 'getDevice').mockReturnValue({
|
||||
temperature_precision: 2,
|
||||
});
|
||||
const message = utils.zigbeeMessage(
|
||||
device, 'msTemperatureMeasurement', 'attReport', {measuredValue: -85}, 1
|
||||
);
|
||||
deviceReceive.onZigbeeMessage(message, device, WSDCGQ11LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], {temperature: -0.85});
|
||||
});
|
||||
expect(publishEntityState).toHaveBeenCalledTimes(1);
|
||||
expect(publishEntityState.mock.calls[0][1]).toStrictEqual({temperature: -0.85});
|
||||
}
|
||||
);
|
||||
|
||||
it('Should handle a zigbee message with voltage 3010', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const message = utils.zigbeeMessage(device, 'genBasic', 'attReport', {'65281': {'1': 3010}}, 1);
|
||||
deviceReceive.onZigbeeMessage(message, device, WXKG02LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
expect(publishEntityState).toHaveBeenCalledTimes(1);
|
||||
const expected = {battery: 100, voltage: 3010};
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], expected);
|
||||
expect(publishEntityState.mock.calls[0][1]).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with voltage 2850', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const message = utils.zigbeeMessage(device, 'genBasic', 'attReport', {'65281': {'1': 2850}}, 1);
|
||||
deviceReceive.onZigbeeMessage(message, device, WXKG02LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
expect(publishEntityState).toHaveBeenCalledTimes(1);
|
||||
const expected = {battery: 35, voltage: 2850};
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], expected);
|
||||
expect(publishEntityState.mock.calls[0][1]).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with voltage 2650', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const message = utils.zigbeeMessage(device, 'genBasic', 'attReport', {'65281': {'1': 2650}}, 1);
|
||||
deviceReceive.onZigbeeMessage(message, device, WXKG02LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
expect(publishEntityState).toHaveBeenCalledTimes(1);
|
||||
const expected = {battery: 14, voltage: 2650};
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], expected);
|
||||
expect(publishEntityState.mock.calls[0][1]).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Should handle a zigbee message with voltage 2000', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const message = utils.zigbeeMessage(device, 'genBasic', 'attReport', {'65281': {'1': 2000}}, 1);
|
||||
deviceReceive.onZigbeeMessage(message, device, WXKG02LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
expect(publishEntityState).toHaveBeenCalledTimes(1);
|
||||
const expected = {battery: 0, voltage: 2000};
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], expected);
|
||||
expect(publishEntityState.mock.calls[0][1]).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Should publish 1 message when converted twice', () => {
|
||||
@@ -182,9 +198,9 @@ describe('DeviceReceive', () => {
|
||||
};
|
||||
const message = utils.zigbeeMessage(device, 'genBasic', 'attReport', payload, 1);
|
||||
deviceReceive.onZigbeeMessage(message, device, RTCGQ11LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
expect(publishEntityState).toHaveBeenCalledTimes(1);
|
||||
const expected = {'battery': 100, 'illuminance': 381, 'voltage': 3045};
|
||||
assert.deepEqual(publishEntityState.getCall(0).args[1], expected);
|
||||
expect(publishEntityState.mock.calls[0][1]).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Should publish no message when converted without result', () => {
|
||||
@@ -192,52 +208,61 @@ describe('DeviceReceive', () => {
|
||||
const payload = {'9999': {'1': 3045}};
|
||||
const message = utils.zigbeeMessage(device, 'genBasic', 'attReport', payload, 1);
|
||||
deviceReceive.onZigbeeMessage(message, device, RTCGQ11LM);
|
||||
assert.isTrue(publishEntityState.notCalled);
|
||||
expect(publishEntityState).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should publish last_seen epoch', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 1);
|
||||
sinon.stub(settings, 'get').callsFake(() => {
|
||||
return {
|
||||
advanced: {
|
||||
last_seen: 'epoch',
|
||||
},
|
||||
};
|
||||
jest.spyOn(settings, 'get').mockReturnValue({
|
||||
advanced: {
|
||||
last_seen: 'epoch',
|
||||
},
|
||||
});
|
||||
deviceReceive.onZigbeeMessage(message, device, WXKG02LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.equal(typeof publishEntityState.getCall(0).args[1].last_seen, 'number');
|
||||
expect(publishEntityState).toHaveBeenCalledTimes(1);
|
||||
expect(typeof publishEntityState.mock.calls[0][1].last_seen).toBe('number');
|
||||
});
|
||||
|
||||
it('Should publish last_seen ISO_8601', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 1);
|
||||
sinon.stub(settings, 'get').callsFake(() => {
|
||||
return {
|
||||
advanced: {
|
||||
last_seen: 'ISO_8601',
|
||||
},
|
||||
};
|
||||
jest.spyOn(settings, 'get').mockReturnValue({
|
||||
advanced: {
|
||||
last_seen: 'ISO_8601',
|
||||
},
|
||||
});
|
||||
deviceReceive.onZigbeeMessage(message, device, WXKG02LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.equal(typeof publishEntityState.getCall(0).args[1].last_seen, 'string');
|
||||
expect(publishEntityState).toHaveBeenCalledTimes(1);
|
||||
expect(typeof publishEntityState.mock.calls[0][1].last_seen).toBe('string');
|
||||
});
|
||||
|
||||
it('Should publish last_seen ISO_8601_local', () => {
|
||||
const device = {ieeeAddr: '0x12345678'};
|
||||
const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 1);
|
||||
sinon.stub(settings, 'get').callsFake(() => {
|
||||
return {
|
||||
advanced: {
|
||||
last_seen: 'ISO_8601_local',
|
||||
},
|
||||
};
|
||||
jest.spyOn(settings, 'get').mockReturnValue({
|
||||
advanced: {
|
||||
last_seen: 'ISO_8601_local',
|
||||
},
|
||||
});
|
||||
deviceReceive.onZigbeeMessage(message, device, WXKG02LM);
|
||||
assert.isTrue(publishEntityState.calledOnce);
|
||||
assert.equal(typeof publishEntityState.getCall(0).args[1].last_seen, 'string');
|
||||
expect(publishEntityState).toHaveBeenCalledTimes(1);
|
||||
expect(typeof publishEntityState.mock.calls[0][1].last_seen).toBe('string');
|
||||
});
|
||||
|
||||
it('Should not handle messages forwarded Xiaomi messages', () => {
|
||||
const device = {ieeeAddr: '0x12345678', manufId: 4151, type: 'Router'};
|
||||
const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 1, 599);
|
||||
deviceReceive.onZigbeeMessage(message, device, ZNCZ02LM);
|
||||
expect(publishEntityState).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should handle messages from Xiaomi router devices', () => {
|
||||
const device = {ieeeAddr: '0x12345678', manufId: 4151, type: 'Router'};
|
||||
const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1});
|
||||
deviceReceive.onZigbeeMessage(message, device, ZNCZ02LM);
|
||||
expect(publishEntityState).toHaveBeenCalledTimes(1);
|
||||
expect(publishEntityState.mock.calls[0][1]).toStrictEqual({state: 'ON'});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+207
-97
@@ -1,7 +1,5 @@
|
||||
const devices = require('zigbee-shepherd-converters').devices;
|
||||
const HomeassistantExtension = require('../lib/extension/homeassistant');
|
||||
const assert = require('chai').assert;
|
||||
const sinon = require('sinon');
|
||||
const settings = require('../lib/util/settings');
|
||||
|
||||
const WSDCGQ11LM = devices.find((d) => d.model === 'WSDCGQ11LM');
|
||||
@@ -13,7 +11,7 @@ describe('HomeAssistant extension', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
mqtt = {
|
||||
publish: sinon.spy(),
|
||||
publish: jest.fn(),
|
||||
};
|
||||
|
||||
homeassistant = new HomeassistantExtension(null, mqtt, null, null);
|
||||
@@ -21,7 +19,7 @@ describe('HomeAssistant extension', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('Should have mapping for all devices supported by zigbee-shepherd-converters', () => {
|
||||
@@ -33,17 +31,16 @@ describe('HomeAssistant extension', () => {
|
||||
}
|
||||
});
|
||||
|
||||
assert.strictEqual(missing.length, 0, `Missing HomeAssistant mapping for: ${missing.join(', ')}`);
|
||||
});
|
||||
expect(missing).toHaveLength(0);
|
||||
}
|
||||
);
|
||||
|
||||
it('Should discover devices', () => {
|
||||
let payload = null;
|
||||
sinon.stub(settings, 'getDevice').callsFake(() => {
|
||||
return {friendly_name: 'my_device'};
|
||||
});
|
||||
jest.spyOn(settings, 'getDevice').mockReturnValue({friendly_name: 'my_device'});
|
||||
|
||||
homeassistant.discover('0x12345678', WSDCGQ11LM, false);
|
||||
assert.equal(mqtt.publish.callCount, 5);
|
||||
expect(mqtt.publish).toHaveBeenCalledTimes(5);
|
||||
|
||||
// 1
|
||||
payload = {
|
||||
@@ -64,10 +61,10 @@ describe('HomeAssistant extension', () => {
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(0).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(0).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(0).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(0).args[4], 'homeassistant');
|
||||
expect(JSON.parse(mqtt.publish.mock.calls[0][1])).toStrictEqual(payload);
|
||||
expect(mqtt.publish.mock.calls[0][2]).toStrictEqual({retain: true, qos: 0});
|
||||
expect(mqtt.publish.mock.calls[0][3]).toBeNull();
|
||||
expect(mqtt.publish.mock.calls[0][4]).toBe('homeassistant');
|
||||
|
||||
// 2
|
||||
payload = {
|
||||
@@ -88,10 +85,10 @@ describe('HomeAssistant extension', () => {
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(1).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(1).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(1).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(1).args[4], 'homeassistant');
|
||||
expect(JSON.parse(mqtt.publish.mock.calls[1][1])).toStrictEqual(payload);
|
||||
expect(mqtt.publish.mock.calls[1][2]).toStrictEqual({retain: true, qos: 0});
|
||||
expect(mqtt.publish.mock.calls[1][3]).toBeNull();
|
||||
expect(mqtt.publish.mock.calls[1][4]).toBe('homeassistant');
|
||||
|
||||
// 3
|
||||
payload = {
|
||||
@@ -112,10 +109,10 @@ describe('HomeAssistant extension', () => {
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(2).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(2).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(2).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(2).args[4], 'homeassistant');
|
||||
expect(JSON.parse(mqtt.publish.mock.calls[2][1])).toStrictEqual(payload);
|
||||
expect(mqtt.publish.mock.calls[2][2]).toStrictEqual({retain: true, qos: 0});
|
||||
expect(mqtt.publish.mock.calls[2][3]).toBeNull();
|
||||
expect(mqtt.publish.mock.calls[2][4]).toBe('homeassistant');
|
||||
|
||||
// 4
|
||||
payload = {
|
||||
@@ -136,10 +133,10 @@ describe('HomeAssistant extension', () => {
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(3).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(3).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(3).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(3).args[4], 'homeassistant');
|
||||
expect(JSON.parse(mqtt.publish.mock.calls[3][1])).toStrictEqual(payload);
|
||||
expect(mqtt.publish.mock.calls[3][2]).toStrictEqual({retain: true, qos: 0});
|
||||
expect(mqtt.publish.mock.calls[3][3]).toBeNull();
|
||||
expect(mqtt.publish.mock.calls[3][4]).toBe('homeassistant');
|
||||
|
||||
// 5
|
||||
payload = {
|
||||
@@ -159,25 +156,23 @@ describe('HomeAssistant extension', () => {
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(4).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(4).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(4).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(4).args[4], 'homeassistant');
|
||||
expect(JSON.parse(mqtt.publish.mock.calls[4][1])).toStrictEqual(payload);
|
||||
expect(mqtt.publish.mock.calls[4][2]).toStrictEqual({retain: true, qos: 0});
|
||||
expect(mqtt.publish.mock.calls[4][3]).toBeNull();
|
||||
expect(mqtt.publish.mock.calls[4][4]).toBe('homeassistant');
|
||||
});
|
||||
|
||||
it('Should discover devices with precision', () => {
|
||||
let payload = null;
|
||||
sinon.stub(settings, 'getDevice').callsFake(() => {
|
||||
return {
|
||||
friendly_name: 'my_device',
|
||||
humidity_precision: 0,
|
||||
temperature_precision: 1,
|
||||
pressure_precision: 2,
|
||||
};
|
||||
jest.spyOn(settings, 'getDevice').mockReturnValue({
|
||||
friendly_name: 'my_device',
|
||||
humidity_precision: 0,
|
||||
temperature_precision: 1,
|
||||
pressure_precision: 2,
|
||||
});
|
||||
|
||||
homeassistant.discover('0x12345678', WSDCGQ11LM, false);
|
||||
assert.equal(mqtt.publish.callCount, 5);
|
||||
expect(mqtt.publish).toHaveBeenCalledTimes(5);
|
||||
|
||||
// 1
|
||||
payload = {
|
||||
@@ -198,10 +193,10 @@ describe('HomeAssistant extension', () => {
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(0).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(0).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(0).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(0).args[4], 'homeassistant');
|
||||
expect(JSON.parse(mqtt.publish.mock.calls[0][1])).toStrictEqual(payload);
|
||||
expect(mqtt.publish.mock.calls[0][2]).toStrictEqual({retain: true, qos: 0});
|
||||
expect(mqtt.publish.mock.calls[0][3]).toBeNull();
|
||||
expect(mqtt.publish.mock.calls[0][4]).toBe('homeassistant');
|
||||
|
||||
// 2
|
||||
payload = {
|
||||
@@ -222,10 +217,10 @@ describe('HomeAssistant extension', () => {
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(1).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(1).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(1).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(1).args[4], 'homeassistant');
|
||||
expect(JSON.parse(mqtt.publish.mock.calls[1][1])).toStrictEqual(payload);
|
||||
expect(mqtt.publish.mock.calls[1][2]).toStrictEqual({retain: true, qos: 0});
|
||||
expect(mqtt.publish.mock.calls[1][3]).toBeNull();
|
||||
expect(mqtt.publish.mock.calls[1][4]).toBe('homeassistant');
|
||||
|
||||
// 3
|
||||
payload = {
|
||||
@@ -246,10 +241,10 @@ describe('HomeAssistant extension', () => {
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(2).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(2).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(2).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(2).args[4], 'homeassistant');
|
||||
expect(JSON.parse(mqtt.publish.mock.calls[2][1])).toStrictEqual(payload);
|
||||
expect(mqtt.publish.mock.calls[2][2]).toStrictEqual({retain: true, qos: 0});
|
||||
expect(mqtt.publish.mock.calls[2][3]).toBeNull();
|
||||
expect(mqtt.publish.mock.calls[2][4]).toBe('homeassistant');
|
||||
|
||||
// 4
|
||||
payload = {
|
||||
@@ -270,10 +265,10 @@ describe('HomeAssistant extension', () => {
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(3).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(3).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(3).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(3).args[4], 'homeassistant');
|
||||
expect(JSON.parse(mqtt.publish.mock.calls[3][1])).toStrictEqual(payload);
|
||||
expect(mqtt.publish.mock.calls[3][2]).toStrictEqual({retain: true, qos: 0});
|
||||
expect(mqtt.publish.mock.calls[3][3]).toBeNull();
|
||||
expect(mqtt.publish.mock.calls[3][4]).toBe('homeassistant');
|
||||
|
||||
// 5
|
||||
payload = {
|
||||
@@ -293,29 +288,27 @@ describe('HomeAssistant extension', () => {
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(4).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(4).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(4).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(4).args[4], 'homeassistant');
|
||||
expect(JSON.parse(mqtt.publish.mock.calls[4][1])).toStrictEqual(payload);
|
||||
expect(mqtt.publish.mock.calls[4][2]).toStrictEqual({retain: true, qos: 0});
|
||||
expect(mqtt.publish.mock.calls[4][3]).toBeNull();
|
||||
expect(mqtt.publish.mock.calls[4][4]).toBe('homeassistant');
|
||||
});
|
||||
|
||||
it('Should discover devices with overriden user configuration', () => {
|
||||
let payload = null;
|
||||
sinon.stub(settings, 'getDevice').callsFake(() => {
|
||||
return {
|
||||
friendly_name: 'my_device',
|
||||
homeassistant: {
|
||||
expire_after: 30,
|
||||
icon: 'mdi:test',
|
||||
temperature: {
|
||||
expire_after: 90,
|
||||
},
|
||||
jest.spyOn(settings, 'getDevice').mockReturnValue({
|
||||
friendly_name: 'my_device',
|
||||
homeassistant: {
|
||||
expire_after: 30,
|
||||
icon: 'mdi:test',
|
||||
temperature: {
|
||||
expire_after: 90,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
homeassistant.discover('0x12345678', WSDCGQ11LM, false);
|
||||
assert.equal(mqtt.publish.callCount, 5);
|
||||
expect(mqtt.publish).toHaveBeenCalledTimes(5);
|
||||
|
||||
// 1
|
||||
payload = {
|
||||
@@ -338,10 +331,10 @@ describe('HomeAssistant extension', () => {
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(0).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(0).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(0).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(0).args[4], 'homeassistant');
|
||||
expect(JSON.parse(mqtt.publish.mock.calls[0][1])).toStrictEqual(payload);
|
||||
expect(mqtt.publish.mock.calls[0][2]).toStrictEqual({retain: true, qos: 0});
|
||||
expect(mqtt.publish.mock.calls[0][3]).toBeNull();
|
||||
expect(mqtt.publish.mock.calls[0][4]).toBe('homeassistant');
|
||||
|
||||
// 2
|
||||
payload = {
|
||||
@@ -364,10 +357,10 @@ describe('HomeAssistant extension', () => {
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(1).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(1).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(1).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(1).args[4], 'homeassistant');
|
||||
expect(JSON.parse(mqtt.publish.mock.calls[1][1])).toStrictEqual(payload);
|
||||
expect(mqtt.publish.mock.calls[1][2]).toStrictEqual({retain: true, qos: 0});
|
||||
expect(mqtt.publish.mock.calls[1][3]).toBeNull();
|
||||
expect(mqtt.publish.mock.calls[1][4]).toBe('homeassistant');
|
||||
|
||||
// 3
|
||||
payload = {
|
||||
@@ -390,10 +383,10 @@ describe('HomeAssistant extension', () => {
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(2).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(2).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(2).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(2).args[4], 'homeassistant');
|
||||
expect(JSON.parse(mqtt.publish.mock.calls[2][1])).toStrictEqual(payload);
|
||||
expect(mqtt.publish.mock.calls[2][2]).toStrictEqual({retain: true, qos: 0});
|
||||
expect(mqtt.publish.mock.calls[2][3]).toBeNull();
|
||||
expect(mqtt.publish.mock.calls[2][4]).toBe('homeassistant');
|
||||
|
||||
// 4
|
||||
payload = {
|
||||
@@ -416,10 +409,10 @@ describe('HomeAssistant extension', () => {
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(3).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(3).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(3).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(3).args[4], 'homeassistant');
|
||||
expect(JSON.parse(mqtt.publish.mock.calls[3][1])).toStrictEqual(payload);
|
||||
expect(mqtt.publish.mock.calls[3][2]).toStrictEqual({retain: true, qos: 0});
|
||||
expect(mqtt.publish.mock.calls[3][3]).toBeNull();
|
||||
expect(mqtt.publish.mock.calls[3][4]).toBe('homeassistant');
|
||||
|
||||
// 5
|
||||
payload = {
|
||||
@@ -441,20 +434,18 @@ describe('HomeAssistant extension', () => {
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(4).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(4).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(4).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(4).args[4], 'homeassistant');
|
||||
expect(JSON.parse(mqtt.publish.mock.calls[4][1])).toStrictEqual(payload);
|
||||
expect(mqtt.publish.mock.calls[4][2]).toStrictEqual({retain: true, qos: 0});
|
||||
expect(mqtt.publish.mock.calls[4][3]).toBeNull();
|
||||
expect(mqtt.publish.mock.calls[4][4]).toBe('homeassistant');
|
||||
});
|
||||
|
||||
it('Should discover devices with cover_position', () => {
|
||||
let payload = null;
|
||||
sinon.stub(settings, 'getDevice').callsFake(() => {
|
||||
return {friendly_name: 'my_device'};
|
||||
});
|
||||
jest.spyOn(settings, 'getDevice').mockReturnValue({friendly_name: 'my_device'});
|
||||
|
||||
homeassistant.discover('0x12345678', SV01, false);
|
||||
assert.equal(mqtt.publish.callCount, 5);
|
||||
expect(mqtt.publish).toHaveBeenCalledTimes(5);
|
||||
|
||||
// 1
|
||||
payload = {
|
||||
@@ -475,9 +466,128 @@ describe('HomeAssistant extension', () => {
|
||||
availability_topic: 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
assert.deepEqual(JSON.parse(mqtt.publish.getCall(0).args[1]), payload);
|
||||
assert.deepEqual(mqtt.publish.getCall(0).args[2], {retain: true, qos: 0});
|
||||
assert.equal(mqtt.publish.getCall(0).args[3], null);
|
||||
assert.equal(mqtt.publish.getCall(0).args[4], 'homeassistant');
|
||||
expect(JSON.parse(mqtt.publish.mock.calls[0][1])).toStrictEqual(payload);
|
||||
expect(mqtt.publish.mock.calls[0][2]).toStrictEqual({retain: true, qos: 0});
|
||||
expect(mqtt.publish.mock.calls[0][3]).toBeNull();
|
||||
expect(mqtt.publish.mock.calls[0][4]).toBe('homeassistant');
|
||||
});
|
||||
|
||||
it('Should discover devices with overriden user configuration in device', () => {
|
||||
let payload = null;
|
||||
jest.spyOn(settings, 'getDevice').mockReturnValue({
|
||||
friendly_name: 'my_device',
|
||||
homeassistant: {
|
||||
device: {
|
||||
identifiers: 'test123',
|
||||
},
|
||||
temperature: {
|
||||
expire_after: 90,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
homeassistant.discover('0x12345678', WSDCGQ11LM, false);
|
||||
expect(mqtt.publish).toHaveBeenCalledTimes(5);
|
||||
|
||||
// 1
|
||||
payload = {
|
||||
'unit_of_measurement': '°C',
|
||||
'device_class': 'temperature',
|
||||
'value_template': '{{ value_json.temperature }}',
|
||||
'json_attributes_topic': 'zigbee2mqtt/my_device',
|
||||
'state_topic': 'zigbee2mqtt/my_device',
|
||||
'name': 'my_device_temperature',
|
||||
'unique_id': '0x12345678_temperature_zigbee2mqtt',
|
||||
'expire_after': 90,
|
||||
'device': {
|
||||
'identifiers': 'test123',
|
||||
'name': 'my_device',
|
||||
'sw_version': 'Zigbee2mqtt test',
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
expect(JSON.parse(mqtt.publish.mock.calls[0][1])).toStrictEqual(payload);
|
||||
expect(mqtt.publish.mock.calls[0][2]).toStrictEqual({retain: true, qos: 0});
|
||||
expect(mqtt.publish.mock.calls[0][3]).toBeNull();
|
||||
expect(mqtt.publish.mock.calls[0][4]).toBe('homeassistant');
|
||||
});
|
||||
|
||||
it('Should discover devices with overriden user configuration in device in temperature', () => {
|
||||
let payload = null;
|
||||
jest.spyOn(settings, 'getDevice').mockReturnValue({
|
||||
friendly_name: 'my_device',
|
||||
homeassistant: {
|
||||
temperature: {
|
||||
expire_after: 90,
|
||||
device: {
|
||||
identifiers: 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
homeassistant.discover('0x12345678', WSDCGQ11LM, false);
|
||||
expect(mqtt.publish).toHaveBeenCalledTimes(5);
|
||||
|
||||
// 1
|
||||
payload = {
|
||||
'unit_of_measurement': '°C',
|
||||
'device_class': 'temperature',
|
||||
'value_template': '{{ value_json.temperature }}',
|
||||
'json_attributes_topic': 'zigbee2mqtt/my_device',
|
||||
'state_topic': 'zigbee2mqtt/my_device',
|
||||
'name': 'my_device_temperature',
|
||||
'unique_id': '0x12345678_temperature_zigbee2mqtt',
|
||||
'expire_after': 90,
|
||||
'device': {
|
||||
'identifiers': 'test',
|
||||
'name': 'my_device',
|
||||
'sw_version': 'Zigbee2mqtt test',
|
||||
'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)',
|
||||
'manufacturer': 'Xiaomi',
|
||||
},
|
||||
'availability_topic': 'zigbee2mqtt/bridge/state',
|
||||
};
|
||||
|
||||
expect(JSON.parse(mqtt.publish.mock.calls[0][1])).toStrictEqual(payload);
|
||||
expect(mqtt.publish.mock.calls[0][2]).toStrictEqual({retain: true, qos: 0});
|
||||
expect(mqtt.publish.mock.calls[0][3]).toBeNull();
|
||||
expect(mqtt.publish.mock.calls[0][4]).toBe('homeassistant');
|
||||
});
|
||||
|
||||
it('Should discover devices with a custom discovery topic', () => {
|
||||
jest.spyOn(settings, 'get').mockReturnValue({
|
||||
mqtt: {
|
||||
base_topic: 'zigbee2mqtt',
|
||||
},
|
||||
experimental: {
|
||||
output: 'json',
|
||||
},
|
||||
advanced: {
|
||||
homeassistant_discovery_topic: 'my_custom_topic',
|
||||
},
|
||||
});
|
||||
|
||||
homeassistant = new HomeassistantExtension(null, mqtt, null, null);
|
||||
|
||||
jest.spyOn(settings, 'getDevice').mockReturnValue({
|
||||
friendly_name: 'my_device',
|
||||
homeassistant: {
|
||||
temperature: {
|
||||
expire_after: 90,
|
||||
device: {
|
||||
identifiers: 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
homeassistant.discover('0x12345678', WSDCGQ11LM, false);
|
||||
|
||||
expect(mqtt.publish).toHaveBeenCalledTimes(5);
|
||||
expect(mqtt.publish.mock.calls[0][4]).toBe('my_custom_topic');
|
||||
});
|
||||
});
|
||||
|
||||
+95
-96
@@ -1,60 +1,62 @@
|
||||
const assert = require('chai').assert;
|
||||
const sinon = require('sinon');
|
||||
const objectAssignDeep = require('object-assign-deep');
|
||||
|
||||
const data = require('../lib/util/data');
|
||||
const tmp = require('tmp');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
const rimraf = require('rimraf');
|
||||
const settings = require('../lib/util/settings.js');
|
||||
const fs = require('../lib/util/fs');
|
||||
|
||||
const configurationFile = data.joinPath('configuration.yaml');
|
||||
const devicesFile = data.joinPath('devices.yaml');
|
||||
const groupsFile = data.joinPath('groups.yaml');
|
||||
|
||||
describe('Settings', () => {
|
||||
let dir = null;
|
||||
const write = (file, json) => fs.writeYaml(file, json);
|
||||
|
||||
const write = (file, json) => {
|
||||
fs.writeFileSync(file, yaml.safeDump(json));
|
||||
};
|
||||
const read = (file) => fs.readYaml(file);
|
||||
|
||||
const read = (file) => {
|
||||
return yaml.safeLoad(fs.readFileSync(file, 'utf8'));
|
||||
};
|
||||
const files = new Map();
|
||||
|
||||
const setup = (configuration) => {
|
||||
const configurationFile = path.join(dir.name, 'configuration.yaml');
|
||||
write(configurationFile, configuration);
|
||||
delete require.cache[require.resolve('../lib/util/settings.js')];
|
||||
const settings = require('../lib/util/settings.js');
|
||||
return {configurationFile, settings};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
dir = tmp.dirSync();
|
||||
sinon.stub(data, 'joinPath').callsFake((file) => {
|
||||
return path.join(dir.name, file);
|
||||
beforeAll(() => {
|
||||
jest.spyOn(fs, 'readYaml').mockImplementation((file) => {
|
||||
if (files.has(file)) return objectAssignDeep.noMutate(files.get(file));
|
||||
throw new Error(`Fake file not found: ${file}`);
|
||||
});
|
||||
jest.spyOn(fs, 'readYamlIfExists').mockImplementation((file) => {
|
||||
if (files.has(file)) return objectAssignDeep.noMutate(files.get(file));
|
||||
return null;
|
||||
});
|
||||
jest.spyOn(fs, 'writeYaml').mockImplementation((file, content) => {
|
||||
files.set(file, objectAssignDeep.noMutate(content));
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rimraf.sync(dir.name);
|
||||
sinon.restore();
|
||||
beforeEach(() => {
|
||||
settings._clear();
|
||||
files.clear();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
fs.readYaml.mockRestore();
|
||||
fs.readYamlIfExists.mockRestore();
|
||||
fs.writeYaml.mockRestore();
|
||||
});
|
||||
|
||||
describe('Settings', () => {
|
||||
it('Should return default settings', () => {
|
||||
const {settings} = setup({});
|
||||
write(configurationFile, {});
|
||||
const s = settings.get();
|
||||
assert.deepEqual(s, settings._getDefaults());
|
||||
expect(s).toStrictEqual(settings._getDefaults());
|
||||
});
|
||||
|
||||
it('Should return settings', () => {
|
||||
const {settings} = setup({permit_join: true});
|
||||
write(configurationFile, {permit_join: true});
|
||||
const s = settings.get();
|
||||
const expected = settings._getDefaults();
|
||||
expected.permit_join = true;
|
||||
assert.deepEqual(s, expected);
|
||||
expect(s).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Should add devices', () => {
|
||||
const {settings, configurationFile} = setup({});
|
||||
write(configurationFile, {});
|
||||
settings.addDevice('0x12345678');
|
||||
|
||||
const actual = read(configurationFile);
|
||||
@@ -67,7 +69,7 @@ describe('Settings', () => {
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(actual, expected);
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Should read devices', () => {
|
||||
@@ -80,7 +82,7 @@ describe('Settings', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const {settings} = setup(content);
|
||||
write(configurationFile, content);
|
||||
|
||||
const device = settings.getDevice('0x12345678');
|
||||
const expected = {
|
||||
@@ -88,10 +90,10 @@ describe('Settings', () => {
|
||||
retain: false,
|
||||
};
|
||||
|
||||
assert.deepEqual(device, expected);
|
||||
expect(device).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Should read devices form a seperate file', () => {
|
||||
it('Should read devices form a separate file', () => {
|
||||
const contentConfiguration = {
|
||||
devices: 'devices.yaml',
|
||||
};
|
||||
@@ -103,37 +105,36 @@ describe('Settings', () => {
|
||||
},
|
||||
};
|
||||
|
||||
write(path.join(dir.name, 'devices.yaml'), contentDevices);
|
||||
const {settings} = setup(contentConfiguration);
|
||||
|
||||
const device = settings.getDevice('0x12345678');
|
||||
const expected = {
|
||||
friendly_name: '0x12345678',
|
||||
retain: false,
|
||||
};
|
||||
|
||||
assert.deepEqual(device, expected);
|
||||
});
|
||||
|
||||
it('Should add devices to a seperate file', () => {
|
||||
const contentConfiguration = {
|
||||
devices: 'devices.yaml',
|
||||
};
|
||||
|
||||
const contentDevices = {
|
||||
'0x12345678': {
|
||||
friendly_name: '0x12345678',
|
||||
retain: false,
|
||||
},
|
||||
};
|
||||
|
||||
const devicesFile = path.join(dir.name, 'devices.yaml');
|
||||
write(devicesFile, contentDevices);
|
||||
const {settings, configurationFile} = setup(contentConfiguration);
|
||||
write(configurationFile, contentConfiguration);
|
||||
|
||||
const device = settings.getDevice('0x12345678');
|
||||
const expected = {
|
||||
friendly_name: '0x12345678',
|
||||
retain: false,
|
||||
};
|
||||
|
||||
expect(device).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Should add devices to a separate file', () => {
|
||||
const contentConfiguration = {
|
||||
devices: 'devices.yaml',
|
||||
};
|
||||
|
||||
const contentDevices = {
|
||||
'0x12345678': {
|
||||
friendly_name: '0x12345678',
|
||||
retain: false,
|
||||
},
|
||||
};
|
||||
|
||||
write(devicesFile, contentDevices);
|
||||
write(configurationFile, contentConfiguration);
|
||||
|
||||
settings.addDevice('0x1234');
|
||||
|
||||
assert.deepEqual(read(configurationFile), {devices: 'devices.yaml'});
|
||||
expect(read(configurationFile)).toStrictEqual({devices: 'devices.yaml'});
|
||||
|
||||
const expected = {
|
||||
'0x12345678': {
|
||||
@@ -146,20 +147,19 @@ describe('Settings', () => {
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(read(devicesFile), expected);
|
||||
expect(read(devicesFile)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Should add devices to a seperate file if devices.yaml doesnt exist', () => {
|
||||
it('Should add devices to a separate file if devices.yaml doesnt exist', () => {
|
||||
const contentConfiguration = {
|
||||
devices: 'devices.yaml',
|
||||
};
|
||||
|
||||
const devicesFile = path.join(dir.name, 'devices.yaml');
|
||||
const {settings, configurationFile} = setup(contentConfiguration);
|
||||
write(configurationFile, contentConfiguration);
|
||||
|
||||
settings.addDevice('0x1234');
|
||||
|
||||
assert.deepEqual(read(configurationFile), {devices: 'devices.yaml'});
|
||||
expect(read(configurationFile)).toStrictEqual({devices: 'devices.yaml'});
|
||||
|
||||
const expected = {
|
||||
'0x1234': {
|
||||
@@ -168,25 +168,26 @@ describe('Settings', () => {
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(read(devicesFile), expected);
|
||||
});
|
||||
expect(read(devicesFile)).toStrictEqual(expected);
|
||||
}
|
||||
);
|
||||
|
||||
it('Should add and remove devices to a seperate file if devices.yaml doesnt exist', () => {
|
||||
it('Should add and remove devices to a separate file if devices.yaml doesnt exist', () => {
|
||||
const contentConfiguration = {
|
||||
devices: 'devices.yaml',
|
||||
};
|
||||
|
||||
const devicesFile = path.join(dir.name, 'devices.yaml');
|
||||
const {settings, configurationFile} = setup(contentConfiguration);
|
||||
write(configurationFile, contentConfiguration);
|
||||
|
||||
settings.addDevice('0x1234');
|
||||
assert.deepEqual(read(configurationFile), {devices: 'devices.yaml'});
|
||||
expect(read(configurationFile)).toStrictEqual({devices: 'devices.yaml'});
|
||||
|
||||
settings.removeDevice('0x1234');
|
||||
assert.deepEqual(read(configurationFile), {devices: 'devices.yaml'});
|
||||
expect(read(configurationFile)).toStrictEqual({devices: 'devices.yaml'});
|
||||
|
||||
assert.deepEqual(read(devicesFile), {});
|
||||
});
|
||||
expect(read(devicesFile)).toStrictEqual({});
|
||||
}
|
||||
);
|
||||
|
||||
it('Should read groups', () => {
|
||||
const content = {
|
||||
@@ -197,17 +198,17 @@ describe('Settings', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const {settings} = setup(content);
|
||||
write(configurationFile, content);
|
||||
|
||||
const group = settings.getGroup('1');
|
||||
const expected = {
|
||||
friendly_name: '123',
|
||||
};
|
||||
|
||||
assert.deepEqual(group, expected);
|
||||
expect(group).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Should read groups form a seperate file', () => {
|
||||
it('Should read groups form a separate file', () => {
|
||||
const contentConfiguration = {
|
||||
groups: 'groups.yaml',
|
||||
};
|
||||
@@ -218,18 +219,18 @@ describe('Settings', () => {
|
||||
},
|
||||
};
|
||||
|
||||
write(path.join(dir.name, 'groups.yaml'), contentGroups);
|
||||
const {settings} = setup(contentConfiguration);
|
||||
write(groupsFile, contentGroups);
|
||||
write(configurationFile, contentConfiguration);
|
||||
|
||||
const group = settings.getGroup('1');
|
||||
const expected = {
|
||||
friendly_name: '123',
|
||||
};
|
||||
|
||||
assert.deepEqual(group, expected);
|
||||
expect(group).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('Combine everything! groups and devices from separte file :)', () => {
|
||||
it('Combine everything! groups and devices from separate file :)', () => {
|
||||
const contentConfiguration = {
|
||||
devices: 'devices.yaml',
|
||||
groups: 'groups.yaml',
|
||||
@@ -241,21 +242,19 @@ describe('Settings', () => {
|
||||
},
|
||||
};
|
||||
|
||||
write(path.join(dir.name, 'groups.yaml'), contentGroups);
|
||||
const {settings, configurationFile} = setup(contentConfiguration);
|
||||
|
||||
const devicesFile = path.join(dir.name, 'devices.yaml');
|
||||
write(groupsFile, contentGroups);
|
||||
write(configurationFile, contentConfiguration);
|
||||
|
||||
const expectedConfiguration = {
|
||||
devices: 'devices.yaml',
|
||||
groups: 'groups.yaml',
|
||||
};
|
||||
|
||||
assert.deepEqual(read(configurationFile), expectedConfiguration);
|
||||
expect(read(configurationFile)).toStrictEqual(expectedConfiguration);
|
||||
|
||||
settings.addDevice('0x1234');
|
||||
|
||||
assert.deepEqual(read(configurationFile), expectedConfiguration);
|
||||
expect(read(configurationFile)).toStrictEqual(expectedConfiguration);
|
||||
|
||||
const expectedDevice = {
|
||||
'0x1234': {
|
||||
@@ -264,23 +263,23 @@ describe('Settings', () => {
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(read(devicesFile), expectedDevice);
|
||||
expect(read(devicesFile)).toStrictEqual(expectedDevice);
|
||||
|
||||
const group = settings.getGroup('1');
|
||||
const expectedGroup = {
|
||||
friendly_name: '123',
|
||||
};
|
||||
|
||||
assert.deepEqual(group, expectedGroup);
|
||||
expect(group).toStrictEqual(expectedGroup);
|
||||
|
||||
assert.deepEqual(read(configurationFile), expectedConfiguration);
|
||||
expect(read(configurationFile)).toStrictEqual(expectedConfiguration);
|
||||
|
||||
const expectedDevice2 = {
|
||||
friendly_name: '0x1234',
|
||||
retain: false,
|
||||
};
|
||||
|
||||
assert.deepEqual(settings.getDevice('0x1234'), expectedDevice2);
|
||||
expect(settings.getDevice('0x1234')).toStrictEqual(expectedDevice2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+7
-7
@@ -1,13 +1,13 @@
|
||||
const logger = require('../lib/util/logger');
|
||||
|
||||
module.exports = {
|
||||
stubLogger: (sandbox) => {
|
||||
sandbox.stub(logger, 'info').callsFake(() => {});
|
||||
sandbox.stub(logger, 'warn').callsFake(() => {});
|
||||
sandbox.stub(logger, 'debug').callsFake(() => {});
|
||||
sandbox.stub(logger, 'error').callsFake(() => {});
|
||||
stubLogger: (jest) => {
|
||||
jest.spyOn(logger, 'info').mockReturnValue(undefined);
|
||||
jest.spyOn(logger, 'warn').mockReturnValue(undefined);
|
||||
jest.spyOn(logger, 'debug').mockReturnValue(undefined);
|
||||
jest.spyOn(logger, 'error').mockReturnValue(undefined);
|
||||
},
|
||||
zigbeeMessage: (device, cid, type, data, epId) => {
|
||||
return {data: {cid, data}, type, endpoints: [{device, epId}]};
|
||||
zigbeeMessage: (device, cid, type, data, epId, groupid=0) => {
|
||||
return {data: {cid, data}, type, groupid, endpoints: [{device, epId}]};
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user