From 3254be354ccbd30bcf36180aeefb5041462f18f0 Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Thu, 20 Dec 2018 21:47:49 +0100 Subject: [PATCH 1/4] Start on group support. #15 --- lib/controller.js | 2 + lib/extension/devicePublish.js | 2 +- lib/extension/groups.js | 86 ++++++++++++++++++++++++++++++++++ lib/util/settings.js | 1 + lib/zigbee.js | 7 ++- 5 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 lib/extension/groups.js diff --git a/lib/controller.js b/lib/controller.js index 8623c330c4..be9d6acef3 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -16,6 +16,7 @@ const ExtensionDeviceConfigure = require('./extension/deviceConfigure'); const ExtensionDeviceReceive = require('./extension/deviceReceive'); const ExtensionMarkOnlineXiaomi = require('./extension/markOnlineXiaomi'); const ExtensionBridgeConfig = require('./extension/bridgeConfig'); +const ExtensionGroups = require('./extension/groups'); class Controller { constructor() { @@ -38,6 +39,7 @@ class Controller { new ExtensionRouterPollXiaomi(this.zigbee, this.mqtt, this.state, this.publishDeviceState), new ExtensionMarkOnlineXiaomi(this.zigbee, this.mqtt, this.state, this.publishDeviceState), new ExtensionBridgeConfig(this.zigbee, this.mqtt, this.state, this.publishDeviceState), + new ExtensionGroups(this.zigbee, this.mqtt, this.state, this.publishDeviceState), ]; if (settings.get().homeassistant) { diff --git a/lib/extension/devicePublish.js b/lib/extension/devicePublish.js index 4131f0cb6f..9b02ed0720 100644 --- a/lib/extension/devicePublish.js +++ b/lib/extension/devicePublish.js @@ -4,7 +4,7 @@ const zigbeeShepherdConverters = require('zigbee-shepherd-converters'); const Queue = require('queue'); const logger = require('../util/logger'); -const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/.+/(set|get)$`); +const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/((?!group).+)/(set|get)$`); const postfixes = ['left', 'right', 'center', 'bottom_left', 'bottom_right', 'top_left', 'top_right']; const maxDepth = 20; diff --git a/lib/extension/groups.js b/lib/extension/groups.js new file mode 100644 index 0000000000..dd440f6db3 --- /dev/null +++ b/lib/extension/groups.js @@ -0,0 +1,86 @@ +const settings = require('../util/settings'); +const logger = require('../util/logger'); + +const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/group/.+/set$`); + +class Groups { + constructor(zigbee, mqtt, state, publishDeviceState) { + this.zigbee = zigbee; + this.mqtt = mqtt; + this.state = state; + this.publishDeviceState = publishDeviceState; + } + + onMQTTConnected() { + this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/group/+/set`); + } + + onZigbeeStarted() { + const groups = settings.get().groups; + + Object.keys(groups).forEach((name) => { + const group = groups[name]; + + const callback = (device, error, rsp) => { + if (!error) { + logger.info(`Added '${device}' to group '${name}' (${group.ID})`); + } else { + logger.error(`Failed to add '${device}' to group '${name}' (${group.ID})`); + } + }; + + group.devices.forEach((device) => { + const ieeeAddr = settings.getIeeeAddrByFriendlyName(device) || device; + this.zigbee.publish( + ieeeAddr, + 'genGroups', + 'add', + 'functional', + {groupid: group.ID, groupname: name}, + null, + null, + (error, rsp) => callback(device, error, rsp), + ); + }); + }); + } + + parseTopic(topic) { + if (!topic.match(topicRegex)) { + return null; + } + + // Remove base from topic + topic = topic.replace(`${settings.get().mqtt.base_topic}/group/`, ''); + + // Parse type from topic + const type = topic.substr(topic.lastIndexOf('/') + 1, topic.length); + + // Remove type from topic + topic = topic.replace(`/${type}`, ''); + + const name = topic; + + return {type: type, name: name}; + } + + onMQTTMessage(topic, message) { + topic = this.parseTopic(topic); + + if (!topic) { + return false; + } + + if (!settings.get().groups.hasOwnProperty(topic.name)) { + logger.error(`Group '${topic.name}' doesn't exist`); + return false; + } + + const groupID = settings.get().groups[topic.name].ID; + logger.info(groupID, message.toString()); + + return true; + } +} + +module.exports = Groups; diff --git a/lib/util/settings.js b/lib/util/settings.js index 29181eacc7..9e3706459a 100644 --- a/lib/util/settings.js +++ b/lib/util/settings.js @@ -10,6 +10,7 @@ const defaults = { mqtt: { include_device_information: false, }, + groups: {}, advanced: { log_directory: path.join(data.getPath(), 'log', '%TIMESTAMP%'), log_level: process.env.DEBUG ? 'debug' : 'info', diff --git a/lib/zigbee.js b/lib/zigbee.js index 72c3e058ca..23a16b837b 100644 --- a/lib/zigbee.js +++ b/lib/zigbee.js @@ -18,6 +18,11 @@ const shepherdSettings = { }, }; +const defaultCfg = { + manufSpec: 0, + disDefaultRsp: 0, +}; + logger.debug(`Using zigbee-shepherd with settings: '${JSON.stringify(shepherdSettings)}'`); class Zigbee { @@ -187,7 +192,7 @@ class Zigbee { return this.shepherd.find(device.ieeeAddr, 1); } - publish(ieeAddr, cid, cmd, cmdType, zclData, cfg, ep, callback) { + publish(ieeAddr, cid, cmd, cmdType, zclData, cfg=defaultCfg, ep, callback) { const device = this.findDevice(ieeAddr, ep); if (!device) { logger.error(`Zigbee cannot publish message to device because '${ieeAddr}' not known by zigbee-shepherd`); From e3d79a4d1728b9d7cfa3d96b7306154db8cc4cb8 Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Fri, 21 Dec 2018 20:06:39 +0100 Subject: [PATCH 2/4] Implement publishing to zigbee groups. https://github.com/Koenkk/zigbee2mqtt/issues/15 --- lib/extension/devicePublish.js | 94 +++++++---- lib/util/settings.js | 9 ++ lib/zigbee.js | 32 ++-- npm-shrinkwrap.json | 26 ++-- package.json | 4 +- test/devicePublish.test.js | 275 +++++++++++++++++++++------------ 6 files changed, 282 insertions(+), 158 deletions(-) diff --git a/lib/extension/devicePublish.js b/lib/extension/devicePublish.js index 9b02ed0720..423faf6cc5 100644 --- a/lib/extension/devicePublish.js +++ b/lib/extension/devicePublish.js @@ -4,10 +4,18 @@ const zigbeeShepherdConverters = require('zigbee-shepherd-converters'); const Queue = require('queue'); const logger = require('../util/logger'); -const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/((?!group).+)/(set|get)$`); +const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/.+/(set|get)$`); const postfixes = ['left', 'right', 'center', 'bottom_left', 'bottom_right', 'top_left', 'top_right']; const maxDepth = 20; +const groupConverters = [ + zigbeeShepherdConverters.toZigbeeConverters.on_off, + zigbeeShepherdConverters.toZigbeeConverters.light_brightness, + zigbeeShepherdConverters.toZigbeeConverters.light_colortemp, + zigbeeShepherdConverters.toZigbeeConverters.light_color, + zigbeeShepherdConverters.toZigbeeConverters.ignore_transition, +]; + class DevicePublish { constructor(zigbee, mqtt, state, publishDeviceState) { this.zigbee = zigbee; @@ -47,10 +55,10 @@ class DevicePublish { topic = topic.replace(`${settings.get().mqtt.base_topic}/`, ''); // Parse type from topic - const type = topic.substr(topic.lastIndexOf('/') + 1, topic.length); + const cmdType = topic.substr(topic.lastIndexOf('/') + 1, topic.length); // Remove type from topic - topic = topic.replace(`/${type}`, ''); + topic = topic.replace(`/${cmdType}`, ''); // Check if we have to deal with a postfix. let postfix = ''; @@ -61,9 +69,15 @@ class DevicePublish { topic = topic.replace(`/${postfix}`, ''); } - const deviceID = topic; + let entityType = 'device'; + if (topic.startsWith('group/')) { + topic = topic.replace('group/', ''); + entityType = 'group'; + } + + const ID = topic; - return {type: type, deviceID: deviceID, postfix: postfix}; + return {cmdType: cmdType, ID: ID, postfix: postfix, entityType: entityType}; } onMQTTMessage(topic, message) { @@ -73,22 +87,43 @@ class DevicePublish { return false; } - // Map friendlyName to ieeeAddr if possible. - const ieeeAddr = settings.getIeeeAddrByFriendlyName(topic.deviceID) || topic.deviceID; - - // Get device - const device = this.zigbee.getDevice(ieeeAddr); - if (!device) { - logger.error(`Failed to find device with ieeAddr: '${ieeeAddr}'`); - return; + // Map friendlyName (ID) to entityID if possible. + let entityID = null; + if (topic.entityType === 'group') { + entityID = settings.getGroupIDByFriendlyName(topic.ID) || topic.ID; + } else if (topic.entityType === 'device') { + entityID = settings.getIeeeAddrByFriendlyName(topic.ID) || topic.ID; } - // Map device to a model - const model = zigbeeShepherdConverters.findByZigbeeModel(device.modelId); - if (!model) { - logger.warn(`Device with modelID '${device.modelId}' is not supported.`); - logger.warn(`Please see: https://koenkk.github.io/zigbee2mqtt/how_tos/how_to_support_new_devices.html`); - return; + // Get entity details + let endpoint = null; + let converters = null; + let device = null; + + if (topic.entityType === 'device') { + device = this.zigbee.getDevice(entityID); + if (!device) { + logger.error(`Failed to find device with ieeAddr: '${entityID}'`); + return; + } + + // Map device to a model + const model = zigbeeShepherdConverters.findByZigbeeModel(device.modelId); + if (!model) { + logger.warn(`Device with modelID '${device.modelId}' is not supported.`); + logger.warn(`Please see: https://koenkk.github.io/zigbee2mqtt/how_tos/how_to_support_new_devices.html`); + return; + } + + // Determine endpoint to publish to. + if (model.hasOwnProperty('ep')) { + const eps = model.ep(device); + endpoint = eps.hasOwnProperty(topic.postfix) ? eps[topic.postfix] : null; + } + + converters = model.toZigbee; + } else if (topic.entityType === 'group') { + converters = groupConverters; } // Convert the MQTT message to a Zigbee message. @@ -100,13 +135,6 @@ class DevicePublish { json = {state: message.toString()}; } - // Determine endpoint to publish to. - let endpoint = null; - if (model.hasOwnProperty('ep')) { - const eps = model.ep(device); - endpoint = eps.hasOwnProperty(topic.postfix) ? eps[topic.postfix] : null; - } - // When brightness is present skip state; brightness also handles state. if (json.hasOwnProperty('brightness') && json.hasOwnProperty('state')) { logger.debug(`Skipping 'state' because of 'brightness'`); @@ -115,14 +143,14 @@ class DevicePublish { // For each key in the JSON message find the matching converter. Object.keys(json).forEach((key) => { - const converter = model.toZigbee.find((c) => c.key.includes(key)); + const converter = converters.find((c) => c.key.includes(key)); if (!converter) { logger.error(`No converter available for '${key}' (${json[key]})`); return; } // Converter didn't return a result, skip - const converted = converter.convert(key, json[key], json, topic.type); + const converted = converter.convert(key, json[key], json, topic.cmdType); if (!converted) { return; } @@ -130,7 +158,8 @@ class DevicePublish { // Add job to queue this.queue.push((queueCallback) => { this.zigbee.publish( - ieeeAddr, + entityID, + topic.entityType, converted.cid, converted.cmd, converted.cmdType, @@ -139,7 +168,8 @@ class DevicePublish { endpoint, (error, rsp) => { // Devices do not report when they go off, this ensures state (on/off) is always in sync. - if (topic.type === 'set' && !error && (key.startsWith('state') || key === 'brightness')) { + if (topic.entityType === 'device' && topic.cmdType === 'set' && + !error && (key.startsWith('state') || key === 'brightness')) { const msg = {}; const _key = topic.postfix ? `state_${topic.postfix}` : 'state'; msg[_key] = key === 'brightness' ? 'ON' : json['state']; @@ -153,14 +183,14 @@ class DevicePublish { // When there is a transition in the message the state of the device gets out of sync. // Therefore; at the end of the transition, read the new state from the device. - if (topic.type === 'set' && converted.zclData.transtime) { + if (topic.cmdType === 'set' && converted.zclData.transtime && topic.entityType === 'device') { const time = converted.zclData.transtime * 100; const getConverted = converter.convert(key, json[key], json, 'get'); setTimeout(() => { // Add job to queue this.queue.push((queueCallback) => { this.zigbee.publish( - ieeeAddr, getConverted.cid, getConverted.cmd, getConverted.cmdType, + entityID, topic.entityType, getConverted.cid, getConverted.cmd, getConverted.cmdType, getConverted.zclData, getConverted.cfg, endpoint, () => queueCallback() ); }); diff --git a/lib/util/settings.js b/lib/util/settings.js index 779fd8fde5..a47d1fda89 100644 --- a/lib/util/settings.js +++ b/lib/util/settings.js @@ -74,6 +74,14 @@ function getIeeeAddrByFriendlyName(friendlyName) { ); } +function getGroupIDByFriendlyName(friendlyName) { + if (!settings.groups || !settings.groups.hasOwnProperty(friendlyName)) { + return null; + } + + return settings.groups[friendlyName].ID; +} + function changeFriendlyName(old, new_) { const ieeeAddr = getIeeeAddrByFriendlyName(old); @@ -95,5 +103,6 @@ module.exports = { removeDevice: (ieeeAddr) => removeDevice(ieeeAddr), getIeeeAddrByFriendlyName: (friendlyName) => getIeeeAddrByFriendlyName(friendlyName), + getGroupIDByFriendlyName: (friendlyName) => getGroupIDByFriendlyName(friendlyName), changeFriendlyName: (old, new_) => changeFriendlyName(old, new_), }; diff --git a/lib/zigbee.js b/lib/zigbee.js index 23a16b837b..4d294772c5 100644 --- a/lib/zigbee.js +++ b/lib/zigbee.js @@ -192,22 +192,34 @@ class Zigbee { return this.shepherd.find(device.ieeeAddr, 1); } - publish(ieeAddr, cid, cmd, cmdType, zclData, cfg=defaultCfg, ep, callback) { - const device = this.findDevice(ieeAddr, ep); - if (!device) { - logger.error(`Zigbee cannot publish message to device because '${ieeAddr}' not known by zigbee-shepherd`); + getGroup(ID) { + return this.shepherd.getGroup(ID); + } + + publish(entityID, entityType, cid, cmd, cmdType, zclData, cfg=defaultCfg, ep, callback) { + let entity = null; + if (entityType === 'device') { + entity = this.findDevice(entityID, ep); + } else if (entityType === 'group') { + entity = this.getGroup(entityID); + } + + if (!entity) { + logger.error( + `Zigbee cannot publish message to ${entityType} because '${entityID}' not known by zigbee-shepherd` + ); return; } logger.info( - `Zigbee publish to '${ieeAddr}', ${cid} - ${cmd} - ` + + `Zigbee publish to ${entityType} '${entityID}', ${cid} - ${cmd} - ` + `${JSON.stringify(zclData)} - ${JSON.stringify(cfg)} - ${ep}` ); const callback_ = (error, rsp) => { if (error) { logger.error( - `Zigbee publish to '${ieeAddr}', ${cid} - ${cmd} - ${JSON.stringify(zclData)} ` + + `Zigbee publish to ${entityType} '${entityID}', ${cid} - ${cmd} - ${JSON.stringify(zclData)} ` + `- ${JSON.stringify(cfg)} - ${ep} ` + `failed with error ${error}`); } @@ -215,10 +227,10 @@ class Zigbee { callback(error, rsp); }; - if (cmdType === 'functional') { - device.functional(cid, cmd, zclData, cfg, callback_); - } else if (cmdType === 'foundation') { - device.foundation(cid, cmd, zclData, cfg, callback_); + if (cmdType === 'functional' && entity.functional) { + entity.functional(cid, cmd, zclData, cfg, callback_); + } else if (cmdType === 'foundation' && entity.foundation) { + entity.foundation(cid, cmd, zclData, cfg, callback_); } else { logger.error(`Unknown zigbee publish cmdType ${cmdType}`); } diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 883471d529..f52f93b459 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -67,9 +67,9 @@ } }, "@babel/parser": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.2.2.tgz", - "integrity": "sha512-UNTmQ5cSLDeBGBl+s7JeowkqIHgmFAGBnLDdIzFmUNSuS5JF0XBcN59jsh/vJO/YjfsBqMxhMjoFGmNExmf0FA==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.2.3.tgz", + "integrity": "sha512-0LyEcVlfCoFmci8mXx8A5oIkpkOgyo8dRHtxBnK9RRBwxO2+JZPNsqtVEZQ7mJFPxnXF9lfmU24mHOPI0qnlkA==", "dev": true }, "@babel/template": { @@ -84,16 +84,16 @@ } }, "@babel/traverse": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.2.2.tgz", - "integrity": "sha512-E5Bn9FSwHpSkUhthw/XEuvFZxIgrqb9M8cX8j5EUQtrUG5DQUy6bFyl7G7iQ1D1Czudor+xkmp81JbLVVM0Sjg==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.2.3.tgz", + "integrity": "sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "@babel/generator": "^7.2.2", "@babel/helper-function-name": "^7.1.0", "@babel/helper-split-export-declaration": "^7.0.0", - "@babel/parser": "^7.2.2", + "@babel/parser": "^7.2.3", "@babel/types": "^7.2.2", "debug": "^4.1.0", "globals": "^11.1.0", @@ -838,7 +838,7 @@ }, "concat-stream": { "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "resolved": "http://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "requires": { "buffer-from": "^1.0.0", @@ -4763,8 +4763,8 @@ } }, "zigbee-shepherd": { - "version": "git+https://github.com/Koenkk/zigbee-shepherd.git#ce52ac4131e2a505af6197b4a26d2b5360e4eb80", - "from": "git+https://github.com/Koenkk/zigbee-shepherd.git#ce52ac4131e2a505af6197b4a26d2b5360e4eb80", + "version": "git+https://github.com/Koenkk/zigbee-shepherd.git#081227b1789a6b416a5ef4a61162d805272f1ef3", + "from": "git+https://github.com/Koenkk/zigbee-shepherd.git#081227b1789a6b416a5ef4a61162d805272f1ef3", "requires": { "areq": "^0.2.0", "busyman": "^0.3.0", @@ -4792,9 +4792,9 @@ } }, "zigbee-shepherd-converters": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/zigbee-shepherd-converters/-/zigbee-shepherd-converters-7.0.4.tgz", - "integrity": "sha512-yFgF+TTQ5j6fjf0GG6JxIM5KsrKdlGnXM9wpJPR7W48WEwUwGAlB4x9ioRYCjWWEvX03ngR5RoOqTdGyr8P8Gw==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/zigbee-shepherd-converters/-/zigbee-shepherd-converters-7.0.5.tgz", + "integrity": "sha512-wXjWbtJzBpttRtDAS/sDT6KEOPjc6Zi19BIDNClb2K0TQbVmlIwpe3nZk5CsEXC1AtKzzyReO6VAUIa1zX8o+A==", "requires": { "debounce": "*", "debug": "3.2.6", diff --git a/package.json b/package.json index a3fa6fed11..c93d86d57c 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,8 @@ "semver": "*", "winston": "2.4.2", "ziee": "*", - "zigbee-shepherd": "git+https://github.com/Koenkk/zigbee-shepherd.git#ce52ac4131e2a505af6197b4a26d2b5360e4eb80", - "zigbee-shepherd-converters": "7.0.4", + "zigbee-shepherd": "git+https://github.com/Koenkk/zigbee-shepherd.git#081227b1789a6b416a5ef4a61162d805272f1ef3", + "zigbee-shepherd-converters": "7.0.5", "zive": "*" }, "devDependencies": { diff --git a/test/devicePublish.test.js b/test/devicePublish.test.js index 7549844c27..5a337bd0b2 100644 --- a/test/devicePublish.test.js +++ b/test/devicePublish.test.js @@ -11,7 +11,7 @@ const mqtt = { const zigbee = { getDevice: null, - publish: sandbox.stub().callsFake((ieeAddr, cid, cmd, cmdType, zclData, cfg, ep, callback) => { + publish: sandbox.stub().callsFake((entityID, entityType, cid, cmd, cmdType, zclData, cfg, ep, callback) => { callback(false, null); }), }; @@ -42,12 +42,13 @@ describe('DevicePublish', () => { devicePublish.onMQTTMessage('zigbee2mqtt/0x12345678/set', JSON.stringify({brightness: '200'})); chai.assert.isTrue(zigbee.publish.calledOnce); chai.assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x12345678'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'genLevelCtrl'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'moveToLevelWithOnOff'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'functional'); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[4], {level: '200', transtime: 0}); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], cfg.default); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], null); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genLevelCtrl'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToLevelWithOnOff'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional'); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], {level: '200', transtime: 0}); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[7], null); }); it('Should publish messages to zigbee devices when brightness is in %', () => { @@ -56,12 +57,13 @@ describe('DevicePublish', () => { devicePublish.onMQTTMessage('zigbee2mqtt/0x12345678/set', JSON.stringify({brightness_percent: '92'})); chai.assert.isTrue(zigbee.publish.calledOnce); chai.assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x12345678'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'genLevelCtrl'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'moveToLevelWithOnOff'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'functional'); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[4], {level: '235', transtime: 0}); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], cfg.default); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], null); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genLevelCtrl'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToLevelWithOnOff'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional'); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], {level: '235', transtime: 0}); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[7], null); }); it('Should publish messages to zigbee devices when brightness is in number', () => { @@ -70,12 +72,13 @@ describe('DevicePublish', () => { devicePublish.onMQTTMessage('zigbee2mqtt/0x12345678/set', JSON.stringify({brightness: 230})); chai.assert.isTrue(zigbee.publish.calledOnce); chai.assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x12345678'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'genLevelCtrl'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'moveToLevelWithOnOff'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'functional'); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[4], {level: 230, transtime: 0}); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], cfg.default); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], null); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genLevelCtrl'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToLevelWithOnOff'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional'); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], {level: 230, transtime: 0}); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[7], null); }); it('Should publish messages to zigbee devices with color_temp', () => { @@ -84,12 +87,13 @@ describe('DevicePublish', () => { devicePublish.onMQTTMessage('zigbee2mqtt/0x12345678/set', JSON.stringify({color_temp: '222'})); chai.assert.isTrue(zigbee.publish.calledOnce); chai.assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x12345678'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'lightingColorCtrl'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'moveToColorTemp'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'functional'); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[4], {colortemp: '222', transtime: 0}); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], cfg.default); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], null); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'lightingColorCtrl'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToColorTemp'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional'); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], {colortemp: '222', transtime: 0}); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[7], null); }); it('Should publish messages to zigbee devices with color_temp in %', () => { @@ -98,12 +102,13 @@ describe('DevicePublish', () => { devicePublish.onMQTTMessage('zigbee2mqtt/0x12345678/set', JSON.stringify({color_temp_percent: '100'})); chai.assert.isTrue(zigbee.publish.calledOnce); chai.assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x12345678'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'lightingColorCtrl'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'moveToColorTemp'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'functional'); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[4], {colortemp: '500', transtime: 0}); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], cfg.default); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], null); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'lightingColorCtrl'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToColorTemp'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional'); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], {colortemp: '500', transtime: 0}); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[7], null); }); it('Should publish messages to zigbee devices with non-default ep', () => { @@ -112,12 +117,13 @@ describe('DevicePublish', () => { devicePublish.onMQTTMessage('zigbee2mqtt/0x12345678/set', JSON.stringify({state: 'OFF'})); chai.assert.isTrue(zigbee.publish.calledOnce); chai.assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x12345678'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'genOnOff'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'off'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'functional'); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[4], {}); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], cfg.default); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[6], 2); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genOnOff'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'off'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional'); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], {}); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[7], 2); }); it('Should publish messages to zigbee devices with non-default ep and postfix', () => { @@ -126,12 +132,13 @@ describe('DevicePublish', () => { devicePublish.onMQTTMessage('zigbee2mqtt/0x12345678/right/set', JSON.stringify({state: 'OFF'})); chai.assert.isTrue(zigbee.publish.calledOnce); chai.assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x12345678'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'genOnOff'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'off'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'functional'); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[4], {}); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], cfg.default); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[6], 3); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genOnOff'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'off'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional'); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], {}); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[7], 3); }); it('Should publish messages to zigbee gledopto with [11,13]', () => { @@ -140,12 +147,13 @@ describe('DevicePublish', () => { devicePublish.onMQTTMessage('zigbee2mqtt/0x12345678/set', JSON.stringify({state: 'OFF'})); chai.assert.isTrue(zigbee.publish.calledOnce); chai.assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x12345678'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'genOnOff'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'off'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'functional'); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[4], {}); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], cfg.default); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[6], null); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genOnOff'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'off'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional'); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], {}); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[7], null); }); it('Should publish messages to zigbee gledopto with [11,12,13]', () => { @@ -154,12 +162,13 @@ describe('DevicePublish', () => { devicePublish.onMQTTMessage('zigbee2mqtt/0x12345678/set', JSON.stringify({state: 'OFF'})); chai.assert.isTrue(zigbee.publish.calledOnce); chai.assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x12345678'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'genOnOff'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'off'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'functional'); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[4], {}); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], cfg.default); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[6], 12); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genOnOff'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'off'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional'); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], {}); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[7], 12); }); it('Should publish messages to zigbee devices with color xy', () => { @@ -168,12 +177,13 @@ describe('DevicePublish', () => { devicePublish.onMQTTMessage('zigbee2mqtt/0x12345678/set', JSON.stringify({color: {x: 100, y: 50}})); chai.assert.isTrue(zigbee.publish.calledOnce); chai.assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x12345678'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'lightingColorCtrl'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'moveToColor'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'functional'); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[4], {colorx: 6553500, colory: 3276750, transtime: 0}); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], cfg.default); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], null); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'lightingColorCtrl'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToColor'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional'); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], {colorx: 6553500, colory: 3276750, transtime: 0}); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[7], null); }); it('Should publish messages to zigbee devices with color rgb', () => { @@ -182,12 +192,13 @@ describe('DevicePublish', () => { devicePublish.onMQTTMessage('zigbee2mqtt/0x12345678/set', JSON.stringify({color: {r: 100, g: 200, b: 10}})); chai.assert.isTrue(zigbee.publish.calledOnce); chai.assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x12345678'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'lightingColorCtrl'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'moveToColor'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'functional'); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[4], {colorx: 17085, colory: 44000, transtime: 0}); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], cfg.default); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], null); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'lightingColorCtrl'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToColor'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional'); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], {colorx: 17085, colory: 44000, transtime: 0}); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[7], null); }); it('Should publish messages to zigbee devices with color rgb string', () => { @@ -196,12 +207,13 @@ describe('DevicePublish', () => { devicePublish.onMQTTMessage('zigbee2mqtt/0x12345678/set', JSON.stringify({color: {rgb: '100,200,10'}})); chai.assert.isTrue(zigbee.publish.calledOnce); chai.assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x12345678'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'lightingColorCtrl'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'moveToColor'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'functional'); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[4], {colorx: 17085, colory: 44000, transtime: 0}); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], cfg.default); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], null); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'lightingColorCtrl'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToColor'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional'); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], {colorx: 17085, colory: 44000, transtime: 0}); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[7], null); }); it('Should publish 1 message when brightness with state is send', () => { @@ -210,12 +222,44 @@ describe('DevicePublish', () => { devicePublish.onMQTTMessage('zigbee2mqtt/0x12345678/set', JSON.stringify({state: 'ON', brightness: '50'})); chai.assert.isTrue(zigbee.publish.calledOnce); chai.assert.strictEqual(zigbee.publish.getCall(0).args[0], '0x12345678'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'genLevelCtrl'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'moveToLevelWithOnOff'); - chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'functional'); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[4], {level: '50', transtime: 0}); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], cfg.default); - chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], null); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'device'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genLevelCtrl'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'moveToLevelWithOnOff'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional'); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], {level: '50', transtime: 0}); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[7], null); + }); + + it('Should publish messages to groups', () => { + sandbox.stub(settings, 'get').callsFake(() => { + return { + mqtt: { + base_topic: 'zigbee2mqtt', + }, + groups: { + group_1: { + ID: 1, + devices: [ + '0x12345678', + '0x12345678', + ], + }, + }, + }; + }); + + zigbee.publish.resetHistory(); + devicePublish.onMQTTMessage('zigbee2mqtt/group/group_1/set', JSON.stringify({state: 'ON'})); + chai.assert.isTrue(zigbee.publish.calledOnce); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[0], 1); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'group'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genOnOff'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'on'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional'); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], {}); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[7], null); }); }); @@ -253,16 +297,18 @@ describe('DevicePublish', () => { it('Should parse set topic', () => { const topic = 'zigbee2mqtt/my_device_id/set'; const parsed = devicePublish.parseTopic(topic); - chai.assert.strictEqual(parsed.type, 'set'); - chai.assert.strictEqual(parsed.deviceID, 'my_device_id'); + chai.assert.strictEqual(parsed.cmdType, 'set'); + chai.assert.strictEqual(parsed.entityType, 'device'); + chai.assert.strictEqual(parsed.ID, 'my_device_id'); chai.assert.strictEqual(parsed.postfix, ''); }); it('Should parse get topic', () => { const topic = 'zigbee2mqtt/my_device_id2/get'; const parsed = devicePublish.parseTopic(topic); - chai.assert.strictEqual(parsed.type, 'get'); - chai.assert.strictEqual(parsed.deviceID, 'my_device_id2'); + chai.assert.strictEqual(parsed.cmdType, 'get'); + chai.assert.strictEqual(parsed.entityType, 'device'); + chai.assert.strictEqual(parsed.ID, 'my_device_id2'); chai.assert.strictEqual(parsed.postfix, ''); }); @@ -277,16 +323,18 @@ describe('DevicePublish', () => { const topic = 'zigbee2mqtt/at/my/home/my_device_id2/get'; const parsed = devicePublish.parseTopic(topic); - chai.assert.strictEqual(parsed.type, 'get'); - chai.assert.strictEqual(parsed.deviceID, 'my_device_id2'); + chai.assert.strictEqual(parsed.cmdType, 'get'); + chai.assert.strictEqual(parsed.entityType, 'device'); + chai.assert.strictEqual(parsed.ID, 'my_device_id2'); chai.assert.strictEqual(parsed.postfix, ''); }); it('Should parse topic with when deviceID has multiple slashes', () => { const topic = 'zigbee2mqtt/floor0/basement/my_device_id2/set'; const parsed = devicePublish.parseTopic(topic); - chai.assert.strictEqual(parsed.type, 'set'); - chai.assert.strictEqual(parsed.deviceID, 'floor0/basement/my_device_id2'); + chai.assert.strictEqual(parsed.cmdType, 'set'); + chai.assert.strictEqual(parsed.entityType, 'device'); + chai.assert.strictEqual(parsed.ID, 'floor0/basement/my_device_id2'); chai.assert.strictEqual(parsed.postfix, ''); }); @@ -301,48 +349,54 @@ describe('DevicePublish', () => { const topic = 'zigbee2mqtt/at/my/basement/floor0/basement/my_device_id2/set'; const parsed = devicePublish.parseTopic(topic); - chai.assert.strictEqual(parsed.type, 'set'); - chai.assert.strictEqual(parsed.deviceID, 'floor0/basement/my_device_id2'); + chai.assert.strictEqual(parsed.cmdType, 'set'); + chai.assert.strictEqual(parsed.entityType, 'device'); + chai.assert.strictEqual(parsed.ID, 'floor0/basement/my_device_id2'); chai.assert.strictEqual(parsed.postfix, ''); }); it('Should parse set with ieeAddr topic', () => { const topic = 'zigbee2mqtt/0x12345689/set'; const parsed = devicePublish.parseTopic(topic); - chai.assert.strictEqual(parsed.type, 'set'); - chai.assert.strictEqual(parsed.deviceID, '0x12345689'); + chai.assert.strictEqual(parsed.cmdType, 'set'); + chai.assert.strictEqual(parsed.entityType, 'device'); + chai.assert.strictEqual(parsed.ID, '0x12345689'); chai.assert.strictEqual(parsed.postfix, ''); }); it('Should parse set with postfix topic', () => { const topic = 'zigbee2mqtt/0x12345689/left/set'; const parsed = devicePublish.parseTopic(topic); - chai.assert.strictEqual(parsed.type, 'set'); - chai.assert.strictEqual(parsed.deviceID, '0x12345689'); + chai.assert.strictEqual(parsed.cmdType, 'set'); + chai.assert.strictEqual(parsed.entityType, 'device'); + chai.assert.strictEqual(parsed.ID, '0x12345689'); chai.assert.strictEqual(parsed.postfix, 'left'); }); it('Should parse set with postfix topic', () => { const topic = 'zigbee2mqtt/0x12345689/right/set'; const parsed = devicePublish.parseTopic(topic); - chai.assert.strictEqual(parsed.type, 'set'); - chai.assert.strictEqual(parsed.deviceID, '0x12345689'); + chai.assert.strictEqual(parsed.cmdType, 'set'); + chai.assert.strictEqual(parsed.entityType, 'device'); + chai.assert.strictEqual(parsed.ID, '0x12345689'); chai.assert.strictEqual(parsed.postfix, 'right'); }); it('Should parse set with postfix topic', () => { const topic = 'zigbee2mqtt/0x12345689/bottom_left/set'; const parsed = devicePublish.parseTopic(topic); - chai.assert.strictEqual(parsed.type, 'set'); - chai.assert.strictEqual(parsed.deviceID, '0x12345689'); + chai.assert.strictEqual(parsed.cmdType, 'set'); + chai.assert.strictEqual(parsed.entityType, 'device'); + chai.assert.strictEqual(parsed.ID, '0x12345689'); chai.assert.strictEqual(parsed.postfix, 'bottom_left'); }); it('Shouldnt parse set with invalid postfix topic', () => { const topic = 'zigbee2mqtt/0x12345689/invalid/set'; const parsed = devicePublish.parseTopic(topic); - chai.assert.strictEqual(parsed.type, 'set'); - chai.assert.strictEqual(parsed.deviceID, '0x12345689/invalid'); + chai.assert.strictEqual(parsed.cmdType, 'set'); + chai.assert.strictEqual(parsed.entityType, 'device'); + chai.assert.strictEqual(parsed.ID, '0x12345689/invalid'); chai.assert.strictEqual(parsed.postfix, ''); }); @@ -357,9 +411,28 @@ describe('DevicePublish', () => { const topic = 'zigbee2mqtt/at/my/home/my/device/in/basement/sensor/bottom_left/get'; const parsed = devicePublish.parseTopic(topic); - chai.assert.strictEqual(parsed.type, 'get'); - chai.assert.strictEqual(parsed.deviceID, 'my/device/in/basement/sensor'); + chai.assert.strictEqual(parsed.cmdType, 'get'); + chai.assert.strictEqual(parsed.entityType, 'device'); + chai.assert.strictEqual(parsed.ID, 'my/device/in/basement/sensor'); chai.assert.strictEqual(parsed.postfix, 'bottom_left'); }); + + it('Should parse group topics', () => { + const topic = 'zigbee2mqtt/group/group_1/set'; + const parsed = devicePublish.parseTopic(topic); + chai.assert.strictEqual(parsed.cmdType, 'set'); + chai.assert.strictEqual(parsed.entityType, 'group'); + chai.assert.strictEqual(parsed.ID, 'group_1'); + chai.assert.strictEqual(parsed.postfix, ''); + }); + + it('Should parse group topics with mutiple slashes', () => { + const topic = 'zigbee2mqtt/group/master/child/set'; + const parsed = devicePublish.parseTopic(topic); + chai.assert.strictEqual(parsed.cmdType, 'set'); + chai.assert.strictEqual(parsed.entityType, 'group'); + chai.assert.strictEqual(parsed.ID, 'master/child'); + chai.assert.strictEqual(parsed.postfix, ''); + }); }); }); From 72f7fc607bb06168033284430c12359ca602c41a Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Fri, 21 Dec 2018 23:56:57 +0100 Subject: [PATCH 3/4] Complete first version of group support. --- docs/information/groups.md | 27 ++++++++++++ lib/extension/devicePublish.js | 11 ++++- lib/extension/groups.js | 81 +++++++++++++++++----------------- lib/util/settings.js | 6 ++- lib/util/utils.js | 1 + test/devicePublish.test.js | 22 ++++++--- 6 files changed, 99 insertions(+), 49 deletions(-) create mode 100644 docs/information/groups.md diff --git a/docs/information/groups.md b/docs/information/groups.md new file mode 100644 index 0000000000..0b8ffee40c --- /dev/null +++ b/docs/information/groups.md @@ -0,0 +1,27 @@ +# Groups +Zigbee2mqtt has support for Zigbee groups. By using Zigbee groups you can control multiple devices simultaneously. + +## Configuration +Add the following to your `configuration.yaml`. + +```yaml +groups: + # ID, each group should have a different numerical ID + '1': + # Name which will be used to control the group + friendly_name: group_1 +``` + +## Adding a device to a group +Send an MQTT message to `zigbee2mqtt/bridge/groups/[GROUP_FRIENDLY_NAME]/add` with payload `DEVICE_FRIENDLY_NAME` + +## Remove a device from a group +Send an MQTT message to `zigbee2mqtt/bridge/groups/[GROUP_FRIENDLY_NAME]/remove` with payload `DEVICE_FRIENDLY_NAME` + +## Controlling +To control a group the following topic should be used. The payload is the same as is used for controlling devices. + +``` +zigbee2mqtt/group/[GROUP_FRIENDLY_NAME]/set +``` + diff --git a/lib/extension/devicePublish.js b/lib/extension/devicePublish.js index 423faf6cc5..d4e80eb032 100644 --- a/lib/extension/devicePublish.js +++ b/lib/extension/devicePublish.js @@ -3,6 +3,7 @@ const settings = require('../util/settings'); const zigbeeShepherdConverters = require('zigbee-shepherd-converters'); const Queue = require('queue'); 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']; @@ -90,7 +91,15 @@ class DevicePublish { // Map friendlyName (ID) to entityID if possible. let entityID = null; if (topic.entityType === 'group') { - entityID = settings.getGroupIDByFriendlyName(topic.ID) || topic.ID; + const groupID = settings.getGroupIDByFriendlyName(topic.ID); + if (groupID) { + entityID = Number(groupID); + } else if (utils.isNumeric(topic.ID)) { + entityID = Number(topic.ID); + } else { + logger.error(`Cannot find group '${topic.ID}'`); + return; + } } else if (topic.entityType === 'device') { entityID = settings.getIeeeAddrByFriendlyName(topic.ID) || topic.ID; } diff --git a/lib/extension/groups.js b/lib/extension/groups.js index dd440f6db3..6d8b2a2c35 100644 --- a/lib/extension/groups.js +++ b/lib/extension/groups.js @@ -1,7 +1,7 @@ const settings = require('../util/settings'); const logger = require('../util/logger'); -const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/group/.+/set$`); +const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/groups/.+/(remove|add)$`); class Groups { constructor(zigbee, mqtt, state, publishDeviceState) { @@ -11,38 +11,13 @@ class Groups { this.publishDeviceState = publishDeviceState; } - onMQTTConnected() { - this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/group/+/set`); + stop() { + this.queue.stop(); } - onZigbeeStarted() { - const groups = settings.get().groups; - - Object.keys(groups).forEach((name) => { - const group = groups[name]; - - const callback = (device, error, rsp) => { - if (!error) { - logger.info(`Added '${device}' to group '${name}' (${group.ID})`); - } else { - logger.error(`Failed to add '${device}' to group '${name}' (${group.ID})`); - } - }; - - group.devices.forEach((device) => { - const ieeeAddr = settings.getIeeeAddrByFriendlyName(device) || device; - this.zigbee.publish( - ieeeAddr, - 'genGroups', - 'add', - 'functional', - {groupid: group.ID, groupname: name}, - null, - null, - (error, rsp) => callback(device, error, rsp), - ); - }); - }); + onMQTTConnected() { + this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/groups/+/remove`); + this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/groups/+/add`); } parseTopic(topic) { @@ -51,7 +26,7 @@ class Groups { } // Remove base from topic - topic = topic.replace(`${settings.get().mqtt.base_topic}/group/`, ''); + topic = topic.replace(`${settings.get().mqtt.base_topic}/bridge/groups/`, ''); // Parse type from topic const type = topic.substr(topic.lastIndexOf('/') + 1, topic.length); @@ -59,9 +34,7 @@ class Groups { // Remove type from topic topic = topic.replace(`/${type}`, ''); - const name = topic; - - return {type: type, name: name}; + return {friendly_name: topic, type: type}; } onMQTTMessage(topic, message) { @@ -71,13 +44,41 @@ class Groups { return false; } - if (!settings.get().groups.hasOwnProperty(topic.name)) { - logger.error(`Group '${topic.name}' doesn't exist`); - return false; + // Find ID of this group. + const groupID = settings.getGroupIDByFriendlyName(topic.friendly_name); + if (!groupID) { + logger.error(`Group with friendly_name '${topic.friendly_name}' doesn't exist`); + return; + } + + // Map message to ieeeAddr and check if device exist. + message = message.toString(); + const ieeeAddr = settings.getIeeeAddrByFriendlyName(message) || message; + if (!this.zigbee.getDevice(ieeeAddr)) { + logger.error(`Failed to find device '${message}'`); + return; + } + + // Send command to the device. + let payload = null; + if (topic.type === 'add') { + payload = {groupid: groupID, groupname: topic.friendly_name}; + } else if (topic.type === 'remove') { + payload = {groupid: groupID}; } - const groupID = settings.get().groups[topic.name].ID; - logger.info(groupID, message.toString()); + const callback = (error, rsp) => { + if (error) { + logger.error(`Failed to ${topic.type} ${ieeeAddr} from ${topic.friendly_name}`); + } else { + logger.error(`Succesfully ${topic.type} ${ieeeAddr} to ${topic.friendly_name}`); + } + }; + + this.zigbee.publish( + ieeeAddr, 'device', 'genGroups', topic.type, 'functional', + payload, null, null, callback, + ); return true; } diff --git a/lib/util/settings.js b/lib/util/settings.js index a47d1fda89..4c801c5572 100644 --- a/lib/util/settings.js +++ b/lib/util/settings.js @@ -75,11 +75,13 @@ function getIeeeAddrByFriendlyName(friendlyName) { } function getGroupIDByFriendlyName(friendlyName) { - if (!settings.groups || !settings.groups.hasOwnProperty(friendlyName)) { + if (!settings.groups) { return null; } - return settings.groups[friendlyName].ID; + return Object.keys(settings.groups).find((ID) => + settings.groups[ID].friendly_name === friendlyName + ); } function changeFriendlyName(old, new_) { diff --git a/lib/util/utils.js b/lib/util/utils.js index f7267c5529..7e3aad0bb8 100644 --- a/lib/util/utils.js +++ b/lib/util/utils.js @@ -5,4 +5,5 @@ module.exports = { millisecondsToSeconds: (milliseconds) => milliseconds / 1000, secondsToMilliseconds: (seconds) => seconds * 1000, isXiaomiDevice: (device) => xiaomiManufacturerID.includes(device.manufId), + isNumeric: (string) => /^\d+$/.test(string), }; diff --git a/test/devicePublish.test.js b/test/devicePublish.test.js index 5a337bd0b2..a255ee8255 100644 --- a/test/devicePublish.test.js +++ b/test/devicePublish.test.js @@ -238,12 +238,8 @@ describe('DevicePublish', () => { base_topic: 'zigbee2mqtt', }, groups: { - group_1: { - ID: 1, - devices: [ - '0x12345678', - '0x12345678', - ], + 1: { + friendly_name: 'group_1', }, }, }; @@ -261,6 +257,20 @@ describe('DevicePublish', () => { chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default); chai.assert.deepEqual(zigbee.publish.getCall(0).args[7], null); }); + + it('Should publish messages to groups by ID', () => { + zigbee.publish.resetHistory(); + devicePublish.onMQTTMessage('zigbee2mqtt/group/2/set', JSON.stringify({state: 'ON'})); + chai.assert.isTrue(zigbee.publish.calledOnce); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[0], 2); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'group'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genOnOff'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'on'); + chai.assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional'); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], {}); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default); + chai.assert.deepEqual(zigbee.publish.getCall(0).args[7], null); + }); }); describe('Parse topic', () => { From 1d51993d8416ec802b9ddce2faf134317f7446e5 Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Sat, 22 Dec 2018 00:04:31 +0100 Subject: [PATCH 4/4] Fix tests. --- test/devicePublish.test.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/test/devicePublish.test.js b/test/devicePublish.test.js index a255ee8255..95428aa5e5 100644 --- a/test/devicePublish.test.js +++ b/test/devicePublish.test.js @@ -232,19 +232,7 @@ describe('DevicePublish', () => { }); it('Should publish messages to groups', () => { - sandbox.stub(settings, 'get').callsFake(() => { - return { - mqtt: { - base_topic: 'zigbee2mqtt', - }, - groups: { - 1: { - friendly_name: 'group_1', - }, - }, - }; - }); - + sandbox.stub(settings, 'getGroupIDByFriendlyName').callsFake(() => '1'); zigbee.publish.resetHistory(); devicePublish.onMQTTMessage('zigbee2mqtt/group/group_1/set', JSON.stringify({state: 'ON'})); chai.assert.isTrue(zigbee.publish.calledOnce);