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/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..d4e80eb032 100644 --- a/lib/extension/devicePublish.js +++ b/lib/extension/devicePublish.js @@ -3,11 +3,20 @@ 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']; 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 +56,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 +70,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 +88,51 @@ 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') { + 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; } - // 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 +144,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 +152,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 +167,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 +177,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 +192,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/extension/groups.js b/lib/extension/groups.js new file mode 100644 index 0000000000..6d8b2a2c35 --- /dev/null +++ b/lib/extension/groups.js @@ -0,0 +1,87 @@ +const settings = require('../util/settings'); +const logger = require('../util/logger'); + +const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/groups/.+/(remove|add)$`); + +class Groups { + constructor(zigbee, mqtt, state, publishDeviceState) { + this.zigbee = zigbee; + this.mqtt = mqtt; + this.state = state; + this.publishDeviceState = publishDeviceState; + } + + stop() { + this.queue.stop(); + } + + 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) { + if (!topic.match(topicRegex)) { + return null; + } + + // Remove base from topic + topic = topic.replace(`${settings.get().mqtt.base_topic}/bridge/groups/`, ''); + + // Parse type from topic + const type = topic.substr(topic.lastIndexOf('/') + 1, topic.length); + + // Remove type from topic + topic = topic.replace(`/${type}`, ''); + + return {friendly_name: topic, type: type}; + } + + onMQTTMessage(topic, message) { + topic = this.parseTopic(topic); + + if (!topic) { + 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 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; + } +} + +module.exports = Groups; diff --git a/lib/util/settings.js b/lib/util/settings.js index 434a22e0c3..4c801c5572 100644 --- a/lib/util/settings.js +++ b/lib/util/settings.js @@ -10,6 +10,7 @@ const defaults = { mqtt: { include_device_information: false, }, + groups: {}, device_options: {}, advanced: { log_directory: path.join(data.getPath(), 'log', '%TIMESTAMP%'), @@ -73,6 +74,16 @@ function getIeeeAddrByFriendlyName(friendlyName) { ); } +function getGroupIDByFriendlyName(friendlyName) { + if (!settings.groups) { + return null; + } + + return Object.keys(settings.groups).find((ID) => + settings.groups[ID].friendly_name === friendlyName + ); +} + function changeFriendlyName(old, new_) { const ieeeAddr = getIeeeAddrByFriendlyName(old); @@ -94,5 +105,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/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/lib/zigbee.js b/lib/zigbee.js index 72c3e058ca..4d294772c5 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,22 +192,34 @@ class Zigbee { return this.shepherd.find(device.ieeeAddr, 1); } - publish(ieeAddr, cid, cmd, cmdType, zclData, cfg, 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}`); } @@ -210,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..95428aa5e5 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,42 @@ 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, 'getGroupIDByFriendlyName').callsFake(() => '1'); + 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); + }); + + 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); }); }); @@ -253,16 +295,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 +321,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 +347,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 +409,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, ''); + }); }); });