Compare commits

..
88 Commits
Author SHA1 Message Date
Koen Kanters 00ebd441a0 1.3.0 2019-04-07 17:33:34 +02:00
Koen Kanters 4843d85fe9 Update zigbee-shepherd-converters to 8.1.1. 2019-04-07 15:38:27 +02:00
Koen Kanters 32db1fd07a Fix acceptDevIncoming. #538 2019-04-01 20:30:27 +02:00
Koen Kanters cee35c66e6 Don’t setup reporting for CC2530 router. #1227 2019-04-01 19:51:14 +02:00
Koen Kanters 76d7861671 Only do read after write when retrieve_state is enabled. #1349 2019-03-30 21:03:54 +01:00
Koen Kanters 35fd1a68fc Update zigbee-shepherd-converters to 8.1.0 (always assume state). #1349 2019-03-30 20:50:40 +01:00
Koen Kanters 8828ee60ae Setup reporting for all attributes when cluster has no attributes. #1064 2019-03-30 18:20:07 +01:00
Koen Kanters 55c3edfa95 Home Assistant: skip state when bulb is already on when setting color or color temperature. #1349 2019-03-30 18:19:10 +01:00
Koen Kanters 2f52b851a9 Add extra acceptDevIncoming logging. #538 2019-03-30 16:48:58 +01:00
Koen Kanters 91bc56a09f Update zigbee-shepherd (cc-znp). #1343 2019-03-29 20:10:02 +01:00
Koen Kanters c737f569f9 Allow already connected devices to rejoin. #1294 #1334 2019-03-29 19:18:15 +01:00
Koen Kanters de4f9f44fd Fix bind by friendly name. #176 2019-03-29 19:10:13 +01:00
Erik van Luijk 21714855bf Pr expcliv2 (#1344)
* Add multiplatform support

Using the experimental manifest support in docker one can
create a manifest that contain references to other images/tags
build for various architectures. When someone pulls koenkk/zigbee2mqtt
correct image is automatically selected.

Please note that experimental cli support needs to be enabled.

* Update docker.sh

* Use docker cli with experimental cli support enabled only when needed
2019-03-29 18:52:45 +01:00
Koen Kanters 50986591c1 Fix tests. 2019-03-28 21:08:18 +01:00
Erik van Luijk e10999f0f2 Add multiplatform support (#1336)
* Add multiplatform support

Using the experimental manifest support in docker one can
create a manifest that contain references to other images/tags
build for various architectures. When someone pulls koenkk/zigbee2mqtt
correct image is automatically selected.

Please note that experimental cli support needs to be enabled.

* Update docker.sh
2019-03-28 20:56:40 +01:00
Koen Kanters 7015741a08 Add dateCode to getDeviceInfoForMqtt. #1213 2019-03-28 19:58:21 +01:00
Bruceforce 64fa02abab Update ISSUE_TEMPLATE.md (#1338)
Handle the "FAQ stuff" as a comment and give the user a basic issue template.
2019-03-28 16:23:41 +01:00
Koen Kanters 7e6dc67861 Update zigbee-shepherd-converters to 8.0.14. 2019-03-27 18:22:40 +01:00
Koen Kanters 3c5727b320 Add dateCode en update zigbee-shepherd. https://github.com/Koenkk/zigbee2mqtt/issues/1213 2019-03-27 18:18:49 +01:00
Koen Kanters 9ef4d2b810 Check if message has endpoints. 2019-03-27 17:31:25 +01:00
Koen Kanters e96dac2596 Log endpoint on zigbee message received. #642 2019-03-27 17:09:55 +01:00
Koen Kanters 9a15e8c118 Improve ban functionallity. #816 2019-03-26 21:40:31 +01:00
Justin Hornosty 25408769e6 Publish and respond to bridge/config/devices/get (#1329)
* - When sent a empty payload to bridge/config/devices/get , publish to
  'bridge/config/devices'
- For bridge/config/devices & bridge/config/devices/get only ever
  publish to the corresponding topic if a empty payload is sent.
- Make sure devicesPublish doesn't treat messages to
  bridge/config/devices/get as a zigbee device.

* fix eslinting errors

* Update bridgeConfig.js
2019-03-26 20:40:50 +01:00
Koen Kanters 2328ecb2ee Update zigbee-shepherd-converters to 8.0.13. 2019-03-25 21:53:39 +01:00
carlos-villanueva b2a453bec3 Add support for smartthings button (model: IM6001-BTP01) (#1312)
* Add support for smartthings button (model: IM6001-BTP01)

* Fix lint
2019-03-24 15:05:56 +01:00
Koen Kanters 20d9bac317 Don’t ping in Xiaomi extension when device availability is enabled. #1248 2019-03-24 14:46:38 +01:00
Koen Kanters 40596bab6a Always ping xiaomi devices through ‘basic’ mechanism. #1248 2019-03-24 14:38:08 +01:00
Willi Eggeling 69d04d0648 shutdown controller on SIGTERM (#1313)
Previously, the controller was only properly shutdown on SIGINT.
SIGTERM is used by kill (unix) and to stop Docker containers.
2019-03-24 14:27:49 +01:00
Koen Kanters 363f4e1c3b Merge branch 'master' into dev 2019-03-24 14:26:49 +01:00
Koen Kanters 008fda4d90 Introduce new ping mechanism (‘basic’) and use it for Xiaomi. https://github.com/Koenkk/zigbee2mqtt/issues/1248 2019-03-23 17:45:28 +01:00
Koen Kanters c64158b75d CC2530/CC2531 routers are not Xiaomi devices. https://github.com/Koenkk/zigbee2mqtt/issues/1248 2019-03-23 17:20:30 +01:00
Koen Kanters 1ea694cb1a Update zigbee-shepherd-convertes to 8.0.12. 2019-03-23 15:32:18 +01:00
Koen Kanters e537784d34 Update zigbee-shepherd-converters to 8.0.11. 2019-03-22 23:37:07 +01:00
Koen Kanters 0277591085 Update zigbee-shepherd-converters to 8.0.10. 2019-03-22 22:45:59 +01:00
Koen Kanters 7b2480be52 Update zigbee-shepherd-conveters to 8.0.9. 2019-03-21 20:34:54 +01:00
Martin Helff 70c30736a1 add more details to device list (#1296) 2019-03-21 20:00:13 +01:00
Koen Kanters 24cf72f166 Update zigbee-shepherd. https://github.com/Koenkk/zigbee2mqtt/issues/1295 2019-03-21 19:59:25 +01:00
Martin Helff e5081b2856 set reportableChange default value to 1 to avoid report flooding (#1293) 2019-03-21 19:43:15 +01:00
Fredrik Lindqvist 4048cb81a4 Update discord link to the #welcome-rules channel (#1290)
New invite link for the new channel structure.
2019-03-20 07:10:37 +01:00
Koen Kanters 2af2a8ea05 Update zigbee-shepherd-converters to 8.0.8. 2019-03-19 20:58:12 +01:00
Oli 1b066d9f59 extend controller.js function getDeviceInfoForMqtt (#1274)
* Add Discord channel. https://github.com/Koenkk/zigbee2mqtt.io/issues/31

* extend controller.js function getDeviceInfoForMqtt

extend controller.js with 
``
zclVersion,
appVersion,
stackVersion,
hwVersion,
swBuildId,
``

* Rework #2

Only Publish hwVersion and swBuildId as this seems to be valid as posible.
Map with default option 'unknown' to be in a good way.

* Fix tests

* Update README.md
2019-03-19 20:29:41 +01:00
Koen Kanters 5c9b31b2fc Don’t create state when cache_state is set to false. #1287 2019-03-19 20:02:17 +01:00
Koen Kanters 61d85af3fd Publish brightness: 0 when turn off trough brightness. https://github.com/Koenkk/zigbee-shepherd-converters/issues/353 2019-03-19 19:43:06 +01:00
Martin Helff 789ec0cc90 query groups through MQTT bridge/config/groups (#1276)
* query groups through MQTT bridge/config/groups

* linter adjustments

* linter adjustments
2019-03-19 19:18:22 +01:00
Koen Kanters b97f5d1afa Add Discourse link. 2019-03-19 19:15:58 +01:00
Koen Kanters 4cb0c0bbb0 Update zigbee-shepherd-converters to 8.0.7. 2019-03-18 18:21:21 +01:00
Stoinov 6d24a49062 Expand attribute output. #493 (#1277)
* Expand attribute output. #493

If attribute is actually an Object, expand it in the format `topic/key-subkey` in order to keep the same amount of topic levels for predictable parsing.
This only expands the first level of contained object. If more expansion are needed a more robust method should be implemented rather than just nesting `if`s.

* Fixing whitespace

fixing https://travis-ci.org/Koenkk/zigbee2mqtt/builds/507616980

* first time splitting line... obviously

Not sure what the proper approach is, but I guess we can put all params on new line.

* identation... really...

How is that `eslint`? any other requests?
2019-03-18 18:17:29 +01:00
Koenkk bee722c9c4 Skip re-transmitted Xiaomi messages. #1238 2019-03-18 17:59:36 +01:00
Koenkk 36bba0154d Fix groupid logging. 2019-03-18 17:32:53 +01:00
Oli a2a90bcdbd Log zigbee2mqtt version information to bridge/config (#1267)
* add coordinator_firmware to bridge/config

* Add zigbee2mqtt version and commit

* Whoops
2019-03-17 20:07:24 +01:00
Koen Kanters 1043053812 Update zigbee-shehperd. 2019-03-17 20:03:07 +01:00
Koen Kanters e6044da604 Implemented device debounce option. #1264 2019-03-16 23:41:46 +01:00
Koen Kanters a8cf925fcb Update zigbee-shepherd, zigbee-shepherd-converters and zcl-id. 2019-03-16 22:55:37 +01:00
Koen Kanters ad835e090b Add Discord channel. https://github.com/Koenkk/zigbee2mqtt.io/issues/31 2019-03-16 20:53:22 +01:00
Koen Kanters 3a96473e2b Improve device bind robustness. https://github.com/Koenkk/zigbee2mqtt/issues/176 2019-03-16 20:07:34 +01:00
Koen Kanters 661f79ce93 Whoops! Fix tests 2019-03-15 23:42:36 +01:00
Koen Kanters 7461fec28b Update zigbee-shepherd-converters to 8.0.4. 2019-03-15 23:33:24 +01:00
Koen Kanters 8b4b9bea19 Refactor property cache responsibility to state. 2019-03-15 23:18:19 +01:00
Koen Kanters fdcf2e8c3d Option to set Home Assistant discovery topic. #1019 2019-03-15 22:41:39 +01:00
Koen Kanters b39e195763 Update zigbee-shepherd. 2019-03-15 22:06:02 +01:00
Koen Kanters f2cdbeab38 Update zigbee-shepherd-converters to 8.0.3. 2019-03-15 21:58:49 +01:00
Koen Kanters f37d52b870 Discover click sensor for QBKG03LM, QBKG04LM, QBKG11LM and QBKG12LM. https://github.com/Koenkk/zigbee2mqtt/issues/723 2019-03-15 21:52:21 +01:00
Koen Kanters 969585c84e Allow to bind specific endpoint. #176 2019-03-15 21:19:42 +01:00
Koen Kanters d4d85633d2 Log groupID. #1238 2019-03-15 19:07:26 +01:00
Koen Kanters f54f3bd40f Add coordinator to zigbee2mqtt/bridge/config/devices. #1236 2019-03-14 20:30:06 +01:00
Koen Kanters 03ca8ec24d Update zigbee-shepherd-converters to 8.0.2. 2019-03-13 20:02:53 +01:00
Koen Kanters a65fbd6e2c Update zigbee-shepherd-converters to 8.0.1. 2019-03-13 19:48:31 +01:00
Koen Kanters ac28ca2841 Update to zigbee-shepherd-converters 8.0.0 2019-03-13 19:41:58 +01:00
Koen Kanters 79d11e295b Fix crash on empty advanced section. https://github.com/Koenkk/zigbee2mqtt/issues/1237 2019-03-12 21:58:45 +01:00
Koen Kanters 9f31cd86a3 Implemented MQTT group add and remove. https://github.com/Koenkk/zigbee2mqtt/issues/764 2019-03-12 21:19:04 +01:00
Koen Kanters c722b611dd Fix livolo queue push. #1241 2019-03-12 20:14:58 +01:00
Roger 3c4fa40a32 Add Securifi Peanut Smart Plug (#1230) 2019-03-11 17:55:25 +01:00
Koen Kanters d81a972f1b Allow to override Home Assistant device discovery payload. https://github.com/Koenkk/zigbee2mqtt/pull/1030 2019-03-10 21:48:40 +01:00
Koen Kanters 0f9333f8b9 Fix elapsed being a string value instead of bool. https://github.com/Koenkk/zigbee2mqtt/pull/1206 2019-03-10 21:34:20 +01:00
Koen Kanters 6e60063337 Update zigbee-shepherd and zigbee-shepherd-converters to 7.2.8. 2019-03-10 14:12:11 +01:00
Andrey 438626691e Allow to configure elapsed via MQTT (#1206)
* Allow to configure elapsed via MQTT

* Update bridgeConfig.js
2019-03-10 14:04:09 +01:00
qm3ster a978b26001 Use toStrictEqual for objects
Not sure why `jest-codemods` didn't do this.
Changed some primitives to use `toBe`.
2019-03-10 13:32:36 +01:00
qm3ster 7cec70aba8 Add @types/jest for editor completion 2019-03-10 13:32:36 +01:00
qm3ster df1825d65e Use mockReturnValue instead of mockImplementation where possible 2019-03-10 13:32:36 +01:00
qm3ster 532b3b2484 Use toHaveBeenNthCalledWith in some cases 2019-03-10 13:32:36 +01:00
qm3ster 84a5277d45 Replace sinon with jest builtins 2019-03-10 13:32:36 +01:00
qm3ster acbab01b7e Apply jest-codemods
`chai` ELIMINATED
2019-03-10 13:32:36 +01:00
qm3ster 6a4073b305 Use jest instead of mocha
Oh nice, coverage works now
2019-03-10 13:32:36 +01:00
Koen Kanters 18aee4803b Merge branch 'master' into dev 2019-03-09 21:45:24 +01:00
qm3ster 3b1fae1452 Remove whitespace 2019-03-09 21:09:39 +01:00
qm3ster cf2f10394a Mock fs in settings test 2019-03-09 21:09:39 +01:00
qm3ster 9eed7e271e Extract IO from settings.js 2019-03-09 21:09:39 +01:00
qm3ster 07765093be Fix all typos of separate 2019-03-09 21:09:39 +01:00
33 changed files with 5404 additions and 3108 deletions
+4 -5
View File
@@ -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"
]
}
+16 -2
View File
@@ -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:
+25
View File
@@ -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
+6
View File
@@ -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>
+1
View File
@@ -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
View File
@@ -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,
};
}
+79 -17
View File
@@ -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);
});
}
}
+8 -6
View File
@@ -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
View File
@@ -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}
);
}
});
});
+1 -5
View File
@@ -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});
}
};
+39 -65
View File
@@ -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(
+53 -18
View File
@@ -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 = {};
+12 -3
View File
@@ -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,
+75 -8
View File
@@ -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));
}
});
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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) {
+16
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
});
});
+2983 -1997
View File
File diff suppressed because it is too large Load Diff
+10 -14
View File
@@ -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"
}
}
+140
View File
@@ -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
View File
@@ -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
View File
@@ -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();
});
+5 -7
View File
@@ -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);
});
});
});
+177
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+115 -90
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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}]};
},
};