From 32112162e7448042746c59722af6bb3b42c8d210 Mon Sep 17 00:00:00 2001 From: Jorge Schrauwen Date: Sat, 12 Oct 2019 21:07:46 +0000 Subject: [PATCH 1/5] Support emulation of attReport Some device do not support attReport for some keys, this will emulate it. You can configure which keys that should be read when another device send a message and the configured device a bind target or in a group the message was send to. ```yaml devices: '0x0017880104259333': friendly_name: bedroom/desk_lamp retain: true debounce: 0.5 report_emulate: - brightness - color ``` Will have the brightness and color queried for example when a hue dimmer sends commands to the bulb. --- lib/extension/deviceReport.js | 89 +++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/lib/extension/deviceReport.js b/lib/extension/deviceReport.js index e04e41322f..fb30658eb4 100644 --- a/lib/extension/deviceReport.js +++ b/lib/extension/deviceReport.js @@ -2,7 +2,9 @@ const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); const logger = require('../util/logger'); const CC2530Router = zigbeeHerdsmanConverters.devices.find((d) => d.model === 'CC2530.ROUTER'); const utils = require('../util/utils'); +const settings = require('../util/settings'); const BaseExtension = require('./baseExtension'); +const debounce = require('debounce'); const defaultConfiguration = { minimumReportInterval: 3, maximumReportInterval: 300, reportableChange: 0, @@ -33,6 +35,7 @@ class DeviceReport extends BaseExtension { super(zigbee, mqtt, state, publishEntityState); this.configuring = new Set(); this.failed = new Set(); + this.emulators = {}; } async setupReporting(device) { @@ -84,6 +87,86 @@ class DeviceReport extends BaseExtension { return true; } + + shouldDoReportingEmulation(device) { + if (device.type == "Coordinator") return false; + + const settingsDevice = settings.getDevice(device.ieeeAddr); + return (settingsDevice && settingsDevice.hasOwnProperty("report_emulate") && + Array.isArray(settingsDevice.report_emulate)); + } + + async handleReportingEmulation(device, group) { + const endpoints = new Set(); + + // lookup endpoints bound to device + for (const endpoint of device.endpoints) { + for (const binding of endpoint.binds) { + if (binding.target.hasOwnProperty('deviceID')) { + if (!endpoints.has(binding.target)) { + endpoints.add(binding.target); + } + } + } + } + + // lookup endpoints part of group (if provided) + if (group) { + for (const endpoint of group.members) { + if (!endpoints.has(endpoint)) { + endpoints.add(endpoint); + } + } + } + + // lookup devices attached to endpoints + if (endpoints.size) { + for (const endpoint of endpoints) { + try { + const endpointDevice = this.zigbee.getDeviceByIeeeAddr(endpoint.deviceIeeeAddress); + if (this.shouldDoReportingEmulation(endpointDevice)) { + // use debounce so we do not flood the network with + // requests to update + if (!this.emulators[endpoint.deviceIeeeAddress]) { + this.emulators[endpoint.deviceIeeeAddress] = debounce(() => { + const settingsDevice = settings.getDevice(endpoint.deviceIeeeAddress); + const model = zigbeeHerdsmanConverters.findByZigbeeModel(endpointDevice.modelID); + if (!model) { + logger.warn(`Could not emulate reporting for ${endpointDevice.ieeeAddr}, unknown device modelID '${endpointDevice.modelID}'`); + return; + } + logger.debug(`Emulating report for ${endpointDevice.ieeeAddr}`); + + const converters = model.toZigbee; + const usedConverters = []; + for (const key of settingsDevice.report_emulate) { + const converter = converters.find((c) => c.key.includes(key)); + + if (converter && converter.convertGet) { + if (usedConverters.includes(converter)) return; + converter.convertGet(endpoint, key, {}); + } else { + logger.error(`Cannot find converter to emulate reporting of '${key}' for '${endpointDevice.ieeeAddr}'`); + } + + usedConverters.push(converter); + } + }, 1000); + } + this.emulators[endpointDevice.ieeeAddr].clear() + this.emulators[endpointDevice.ieeeAddr]() + } + } catch (error) { + logger.error( + `Failed to emulate reporting for '${endpoint.deviceIeeeAddress}' - ${error.stack}` + ); + } + } + } else { + logger.debug(`No devices endpoints discovered, no reporting emulation required`); + } + } + async onZigbeeStarted() { this.coordinatorEndpoint = this.zigbee.getDevicesByType('Coordinator')[0].endpoints[0]; @@ -99,6 +182,12 @@ class DeviceReport extends BaseExtension { if (this.shouldSetupReporting(mappedDevice, data.device, type)) { this.setupReporting(data.device); } + if (type == "message" && (data.type != "attributeReport" && + data.type != "readResponse" && + data.type != "raw")) { + const group = data.groupID > 0 ? this.zigbee.getGroupByID(data.groupID) : null; + this.handleReportingEmulation(data.device, group); + } } } From 45a7b304fe069dfdcfe870f3d93e8aea6720ac95 Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Wed, 23 Oct 2019 20:00:03 +0200 Subject: [PATCH 2/5] Refactor polling. --- lib/extension/deviceReport.js | 161 ++++++++++++++++------------------ 1 file changed, 74 insertions(+), 87 deletions(-) diff --git a/lib/extension/deviceReport.js b/lib/extension/deviceReport.js index fb30658eb4..3a35edaef4 100644 --- a/lib/extension/deviceReport.js +++ b/lib/extension/deviceReport.js @@ -2,9 +2,9 @@ const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); const logger = require('../util/logger'); const CC2530Router = zigbeeHerdsmanConverters.devices.find((d) => d.model === 'CC2530.ROUTER'); const utils = require('../util/utils'); -const settings = require('../util/settings'); const BaseExtension = require('./baseExtension'); const debounce = require('debounce'); +const ZigbeeHerdsman = require('zigbee-herdsman'); const defaultConfiguration = { minimumReportInterval: 3, maximumReportInterval: 300, reportableChange: 0, @@ -30,12 +30,31 @@ const clusters = { ], }; +const pollOnMessage = [ + { + // Key is used this.pollDebouncers uniqueness + key: 1, + // On messages that have the cluster and type of below + cluster: { + manuSpecificPhilips: ['commandHueNotification'], + genLevelCtrl: [ + 'commandStep', 'commandStepWithOnOff', 'commandStop', 'commandMoveWithOnOff', 'commandStopWithOnOff', + 'commandMove', + ], + }, + // Read the following attributes + read: {cluster: 'genLevelCtrl', attributes: ['currentLevel']}, + // When the bound devices have the following manufacturerID + manufacturerID: ZigbeeHerdsman.Zcl.ManufacturerCode.Philips, + }, +]; + class DeviceReport extends BaseExtension { constructor(zigbee, mqtt, state, publishEntityState) { super(zigbee, mqtt, state, publishEntityState); this.configuring = new Set(); this.failed = new Set(); - this.emulators = {}; + this.pollDebouncers = {}; } async setupReporting(device) { @@ -87,86 +106,6 @@ class DeviceReport extends BaseExtension { return true; } - - shouldDoReportingEmulation(device) { - if (device.type == "Coordinator") return false; - - const settingsDevice = settings.getDevice(device.ieeeAddr); - return (settingsDevice && settingsDevice.hasOwnProperty("report_emulate") && - Array.isArray(settingsDevice.report_emulate)); - } - - async handleReportingEmulation(device, group) { - const endpoints = new Set(); - - // lookup endpoints bound to device - for (const endpoint of device.endpoints) { - for (const binding of endpoint.binds) { - if (binding.target.hasOwnProperty('deviceID')) { - if (!endpoints.has(binding.target)) { - endpoints.add(binding.target); - } - } - } - } - - // lookup endpoints part of group (if provided) - if (group) { - for (const endpoint of group.members) { - if (!endpoints.has(endpoint)) { - endpoints.add(endpoint); - } - } - } - - // lookup devices attached to endpoints - if (endpoints.size) { - for (const endpoint of endpoints) { - try { - const endpointDevice = this.zigbee.getDeviceByIeeeAddr(endpoint.deviceIeeeAddress); - if (this.shouldDoReportingEmulation(endpointDevice)) { - // use debounce so we do not flood the network with - // requests to update - if (!this.emulators[endpoint.deviceIeeeAddress]) { - this.emulators[endpoint.deviceIeeeAddress] = debounce(() => { - const settingsDevice = settings.getDevice(endpoint.deviceIeeeAddress); - const model = zigbeeHerdsmanConverters.findByZigbeeModel(endpointDevice.modelID); - if (!model) { - logger.warn(`Could not emulate reporting for ${endpointDevice.ieeeAddr}, unknown device modelID '${endpointDevice.modelID}'`); - return; - } - logger.debug(`Emulating report for ${endpointDevice.ieeeAddr}`); - - const converters = model.toZigbee; - const usedConverters = []; - for (const key of settingsDevice.report_emulate) { - const converter = converters.find((c) => c.key.includes(key)); - - if (converter && converter.convertGet) { - if (usedConverters.includes(converter)) return; - converter.convertGet(endpoint, key, {}); - } else { - logger.error(`Cannot find converter to emulate reporting of '${key}' for '${endpointDevice.ieeeAddr}'`); - } - - usedConverters.push(converter); - } - }, 1000); - } - this.emulators[endpointDevice.ieeeAddr].clear() - this.emulators[endpointDevice.ieeeAddr]() - } - } catch (error) { - logger.error( - `Failed to emulate reporting for '${endpoint.deviceIeeeAddress}' - ${error.stack}` - ); - } - } - } else { - logger.debug(`No devices endpoints discovered, no reporting emulation required`); - } - } - async onZigbeeStarted() { this.coordinatorEndpoint = this.zigbee.getDevicesByType('Coordinator')[0].endpoints[0]; @@ -182,11 +121,59 @@ class DeviceReport extends BaseExtension { if (this.shouldSetupReporting(mappedDevice, data.device, type)) { this.setupReporting(data.device); } - if (type == "message" && (data.type != "attributeReport" && - data.type != "readResponse" && - data.type != "raw")) { - const group = data.groupID > 0 ? this.zigbee.getGroupByID(data.groupID) : null; - this.handleReportingEmulation(data.device, group); + + if (type === 'message') { + this.poll(data); + } + } + + poll(message) { + /** + * This method poll bound endpoints for state changes. + * + * A use case is e.g. a Hue Dimmer switch bound to a Hue bulb. + * Hue bulbs only report their on/off state. + * When dimming the bulb via the dimmer switch the state is therefore not reported. + * When we receive a message from a Hue dimmer we read the brightness from the bulb (if bound). + */ + + const polls = pollOnMessage.filter((p) => + p.cluster[message.cluster] && p.cluster[message.cluster].includes(message.type) + ); + + if (polls.length) { + let toPoll = []; + + // Add bound devices + toPoll = toPoll.concat([].concat(...message.device.endpoints.map((e) => e.binds.map((e) => e)))); + toPoll = toPoll.filter((e) => e.target.constructor.name === 'Endpoint'); + toPoll = toPoll.filter((e) => e.target.getDevice().type !== 'Coordinator'); + toPoll = toPoll.map((e) => e.target); + + // If message is published to a group, add members of the group + const group = message.groupID !== 0 ? this.zigbee.getGroupByID(message.groupID) : null; + if (group) { + toPoll = toPoll.concat(group.members); + } + + toPoll = new Set(toPoll); + + for (const endpoint of toPoll) { + for (const poll of polls) { + if (poll.manufacturerID !== endpoint.getDevice().manufacturerID) { + continue; + } + + const key = `${endpoint.deviceIeeeAddress}_${endpoint.ID}_${poll.key}`; + if (!this.pollDebouncers[key]) { + this.pollDebouncers[key] = debounce(async () => { + await endpoint.read(poll.read.cluster, poll.read.attributes); + }, 1000); + } + + this.pollDebouncers[key](); + } + } } } } From c18e9fb57be627391da260271c0126cde4f9970d Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Wed, 23 Oct 2019 22:09:47 +0200 Subject: [PATCH 3/5] Finish poll tests. --- lib/extension/deviceReport.js | 4 ++-- npm-shrinkwrap.json | 30 ++++++++++++++--------------- package.json | 2 +- test/bridgeConfig.test.js | 2 +- test/deviceReport.test.js | 36 ++++++++++++++++++++++++++++++++++- test/entityPublish.test.js | 4 ++-- test/stub/data.js | 17 +++++++++++++++++ test/stub/zigbeeHerdsman.js | 24 ++++++++++++++++------- 8 files changed, 90 insertions(+), 29 deletions(-) diff --git a/lib/extension/deviceReport.js b/lib/extension/deviceReport.js index 3a35edaef4..43721fc39f 100644 --- a/lib/extension/deviceReport.js +++ b/lib/extension/deviceReport.js @@ -44,7 +44,7 @@ const pollOnMessage = [ }, // Read the following attributes read: {cluster: 'genLevelCtrl', attributes: ['currentLevel']}, - // When the bound devices have the following manufacturerID + // When the bound devices/members of group have the following manufacturerID manufacturerID: ZigbeeHerdsman.Zcl.ManufacturerCode.Philips, }, ]; @@ -129,7 +129,7 @@ class DeviceReport extends BaseExtension { poll(message) { /** - * This method poll bound endpoints for state changes. + * This method poll bound endpoints and group members for state changes. * * A use case is e.g. a Hue Dimmer switch bound to a Hue bulb. * Hue bulbs only report their on/off state. diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 4ce5412a43..28a9cfbd5a 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -880,9 +880,9 @@ } }, "bser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.0.tgz", - "integrity": "sha512-8zsjWrQkkBoLK6uxASk1nJ2SKv97ltiGDo6A3wA0/yRPz+CwmEyDo0hUrhIuukG2JHpAl3bvFIixw2/3Hi0DOg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "dev": true, "requires": { "node-int64": "^0.4.0" @@ -1628,9 +1628,9 @@ "dev": true }, "eslint-plugin-jest": { - "version": "22.19.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-22.19.0.tgz", - "integrity": "sha512-4zUc3rh36ds0SXdl2LywT4YWA3zRe8sfLhz8bPp8qQPIKvynTTkNGwmSCMpl5d9QiZE2JxSinGF+WD8yU+O0Lg==", + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-22.20.0.tgz", + "integrity": "sha512-UwHGXaYprxwd84Wer8H7jZS+5C3LeEaU8VD7NqORY6NmPJrs+9Ugbq3wyjqO3vWtSsDaLar2sqEB8COmOZA4zw==", "dev": true, "requires": { "@typescript-eslint/experimental-utils": "^1.13.0" @@ -4644,9 +4644,9 @@ "dev": true }, "react-is": { - "version": "16.10.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.2.tgz", - "integrity": "sha512-INBT1QEgtcCCgvccr5/86CfD71fw9EPmDxgiJX4I2Ddr6ZsV6iFXsuby+qWJPtmNuMY0zByTsG4468P7nHuNWA==", + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.11.0.tgz", + "integrity": "sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw==", "dev": true }, "read-pkg": { @@ -5665,9 +5665,9 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "uglify-js": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.3.tgz", - "integrity": "sha512-KfQUgOqTkLp2aZxrMbCuKCDGW9slFYu2A23A36Gs7sGzTLcRBDORdOi5E21KWHFIfkY8kzgi/Pr1cXCh0yIp5g==", + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.4.tgz", + "integrity": "sha512-9Yc2i881pF4BPGhjteCXQNaXx1DCwm3dtOyBaG2hitHjLWOczw/ki8vD1bqyT3u6K0Ms/FpCShkmfg+FtlOfYA==", "dev": true, "optional": true, "requires": { @@ -6053,9 +6053,9 @@ } }, "zigbee-herdsman": { - "version": "0.10.9", - "resolved": "https://registry.npmjs.org/zigbee-herdsman/-/zigbee-herdsman-0.10.9.tgz", - "integrity": "sha512-QcV/v1hqGYdlawvvgoZsClOI6aeXGvJVSCZkMm4xQS1SbevTXzXFP4VtcR5WlnK0a+Kl7ibXcVfnR4XCl2r5IA==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/zigbee-herdsman/-/zigbee-herdsman-0.11.0.tgz", + "integrity": "sha512-T14/9zWUk88kEMzbjQTV1/qZEWXG6ZE8EQZVgcpqlol8KQIICj5BblR0g9VNuRmI6Q4vy4eGUSUVVlVy5R2wLg==", "requires": { "debug": "^4.1.1", "fast-deep-equal": "^2.0.1", diff --git a/package.json b/package.json index 6054a202a5..9e84d95959 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "rimraf": "*", "semver": "*", "winston": "*", - "zigbee-herdsman": "0.10.9", + "zigbee-herdsman": "0.11.0", "zigbee-herdsman-converters": "11.1.28" }, "devDependencies": { diff --git a/test/bridgeConfig.test.js b/test/bridgeConfig.test.js index a179d91bd4..f0485e3af1 100644 --- a/test/bridgeConfig.test.js +++ b/test/bridgeConfig.test.js @@ -170,7 +170,7 @@ describe('Bridge config', () => { await flushPromises(); expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); const payload = JSON.parse(MQTT.publish.mock.calls[0][1]); - expect(payload).toStrictEqual({"message": [{"ID": 1, "friendly_name": "group_1", "retain": false, 'devices': [], optimistic: true}, {"ID": 2, "friendly_name": "group_2", "retain": false, "devices": [], optimistic: true}], "type": "groups"}); + expect(payload).toStrictEqual({"message": [{"ID": 1, "friendly_name": "group_1", "retain": false, 'devices': [], optimistic: true}, {"ID": 2, "friendly_name": "group_2", "retain": false, "devices": [], optimistic: true}, {"ID": 15071, "friendly_name": "group_tradfri_remote", "retain": false, "devices": ['bulb_color_2', 'bulb_2'], optimistic: true}], "type": "groups"}); }); it('Should allow rename devices', async () => { diff --git a/test/deviceReport.test.js b/test/deviceReport.test.js index 7e68c9c9ad..1026fcc304 100644 --- a/test/deviceReport.test.js +++ b/test/deviceReport.test.js @@ -6,13 +6,17 @@ zigbeeHerdsman.returnDevices.push('0x00124b00120144ae'); zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b2'); zigbeeHerdsman.returnDevices.push('0x0017880104e45553'); zigbeeHerdsman.returnDevices.push('0x0017880104e45559'); +zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b4'); +zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b7'); const MQTT = require('./stub/mqtt'); const settings = require('../lib/util/settings'); const Controller = require('../lib/controller'); const flushPromises = () => new Promise(setImmediate); const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +jest.mock('debounce', () => jest.fn(fn => fn)); +const debounce = require('debounce'); -const mocksClear = [MQTT.publish, logger.warn, logger.debug]; +const mocksClear = [MQTT.publish, logger.warn, logger.debug, debounce]; describe('Device report', () => { let controller; @@ -163,4 +167,34 @@ describe('Device report', () => { await flushPromises(); expect(endpoint.bind).toHaveBeenCalledTimes(1); }); + + it('Should poll bounded Hue bulb when receiving message from Hue dimmer', async () => { + const remote = zigbeeHerdsman.devices.remote; + const data = {"button":3,"unknown1":3145728,"type":2,"unknown2":0,"time":1}; + const payload = {data, cluster: 'manuSpecificPhilips', device: remote, endpoint: remote.getEndpoint(2), type: 'commandHueNotification', linkquality: 10, groupID: 0}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(debounce).toHaveBeenCalledTimes(1); + expect(zigbeeHerdsman.devices.bulb_color.getEndpoint(1).read).toHaveBeenCalledWith("genLevelCtrl", ["currentLevel"]); + }); + + it('Should poll grouped Hue bulb when receiving message from TRADFRI remote and should', async () => { + const remote = zigbeeHerdsman.devices.tradfri_remote; + const data = {"stepmode":0,"stepsize":43,"transtime":5}; + const payload = {data, cluster: 'genLevelCtrl', device: remote, endpoint: remote.getEndpoint(1), type: 'commandStepWithOnOff', linkquality: 10, groupID: 15071}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(debounce).toHaveBeenCalledTimes(1); + expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledTimes(1); + expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledWith("genLevelCtrl", ["currentLevel"]); + + // Should also only debounce once + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(debounce).toHaveBeenCalledTimes(1); + expect(zigbeeHerdsman.devices.bulb_color_2.getEndpoint(1).read).toHaveBeenCalledTimes(2); + + // Should only call Hue bulb, not e.g. tradfri + expect(zigbeeHerdsman.devices.bulb.getEndpoint(1).read).toHaveBeenCalledTimes(0); + }); }); diff --git a/test/entityPublish.test.js b/test/entityPublish.test.js index a052857827..021d9c885f 100644 --- a/test/entityPublish.test.js +++ b/test/entityPublish.test.js @@ -353,10 +353,10 @@ describe('Entity publish', () => { it('Should create and publish to group which is in configuration.yaml but not in zigbee-herdsman', async () => { delete zigbeeHerdsman.groups.group_2; - expect(Object.values(zigbeeHerdsman.groups).length).toBe(1); + expect(Object.values(zigbeeHerdsman.groups).length).toBe(2); await MQTT.events.message('zigbee2mqtt/group_2/set', JSON.stringify({state: 'ON'})); await flushPromises(); - expect(Object.values(zigbeeHerdsman.groups).length).toBe(2); + expect(Object.values(zigbeeHerdsman.groups).length).toBe(3); expect(zigbeeHerdsman.groups.group_2.command).toHaveBeenCalledTimes(1); expect(zigbeeHerdsman.groups.group_2.command).toHaveBeenCalledWith("genOnOff", "on", {}, {}); }); diff --git a/test/stub/data.js b/test/stub/data.js index f764802327..28976fd92a 100644 --- a/test/stub/data.js +++ b/test/stub/data.js @@ -55,10 +55,18 @@ function writeDefaultConfiguration() { retain: false, friendly_name: "ikea_onoff" }, + '0x000b57fffec6a5b7': { + retain: false, + friendly_name: "bulb_2" + }, "0x000b57fffec6a5b3": { retain: false, friendly_name: "bulb_color" }, + '0x000b57fffec6a5b4': { + retain: false, + friendly_name: "bulb_color_2" + }, "0x0017880104e45541": { retain: false, friendly_name: "wall_switch" @@ -118,6 +126,10 @@ function writeDefaultConfiguration() { '0x0017880104e45560': { retain: false, friendly_name: 'livolo' + }, + '0x90fd9ffffe4b64ae': { + retain: false, + friendly_name: 'tradfri_remote', } }, groups: { @@ -128,6 +140,11 @@ function writeDefaultConfiguration() { '2': { friendly_name: 'group_2', retain: false, + }, + '15071': { + friendly_name: 'group_tradfri_remote', + retain: false, + devices: ['bulb_color_2', 'bulb_2'] } } }; diff --git a/test/stub/zigbeeHerdsman.js b/test/stub/zigbeeHerdsman.js index 3142a02dd2..f0a8decf39 100644 --- a/test/stub/zigbeeHerdsman.js +++ b/test/stub/zigbeeHerdsman.js @@ -2,11 +2,11 @@ const events = {}; const assert = require('assert'); class Group { - constructor(groupID) { + constructor(groupID, members) { this.groupID = groupID; this.command = jest.fn(); this.meta = {}; - this.members = []; + this.members = members; this.hasMember = (endpoint) => this.members.includes(endpoint); } } @@ -20,7 +20,7 @@ const clusters = { } class Endpoint { - constructor(ID, inputClusters, outputClusters, deviceIeeeAddress) { + constructor(ID, inputClusters, outputClusters, deviceIeeeAddress, binds=[]) { this.deviceIeeeAddress = deviceIeeeAddress; this.ID = ID; this.inputClusters = inputClusters; @@ -31,6 +31,7 @@ class Endpoint { this.bind = jest.fn(); this.unbind = jest.fn(); this.configureReporting = jest.fn(); + this.binds = binds; this.supportsInputCluster = (cluster) => { assert(clusters[cluster], `Undefined '${cluster}'`); return this.inputClusters.includes(clusters[cluster]); @@ -88,11 +89,17 @@ class Device { const returnDevices = []; +const bulb_color = new Device('Router', '0x000b57fffec6a5b3', 40399, 4107, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b3')], true, "Mains (single phase)", "LLC020"); +const bulb_color_2 = new Device('Router', '0x000b57fffec6a5b4', 401292, 4107, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b4')], true, "Mains (single phase)", "LLC020"); +const bulb_2 = new Device('Router', '0x000b57fffec6a5b7', 40369, 4476, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b7')], true, "Mains (single phase)", "TRADFRI bulb E27 WS opal 980lm"); + const devices = { 'coordinator': new Device('Coordinator', '0x00124b00120144ae', 0, 0, [new Endpoint(1, [], [])], false), 'bulb': new Device('Router', '0x000b57fffec6a5b2', 40369, 4476, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b2')], true, "Mains (single phase)", "TRADFRI bulb E27 WS opal 980lm"), - 'bulb_color': new Device('Router', '0x000b57fffec6a5b3', 40399, 6535, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b3')], true, "Mains (single phase)", "LLC020"), - 'remote': new Device('EndDevice', '0x0017880104e45517', 6535, 4107, [new Endpoint(1, [0], [0,3,4,6,8,5]), new Endpoint(2, [0,1,3,15,64512], [25, 6])], true, "Battery", "RWL021"), + 'bulb_color': bulb_color, + 'bulb_2': bulb_2, + 'bulb_color_2': bulb_color_2, + 'remote': new Device('EndDevice', '0x0017880104e45517', 6535, 4107, [new Endpoint(1, [0], [0,3,4,6,8,5], '0x0017880104e45517', [{target: bulb_color.endpoints[0]}]), new Endpoint(2, [0,1,3,15,64512], [25, 6])], true, "Battery", "RWL021"), 'unsupported': new Device('EndDevice', '0x0017880104e45518', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", "notSupportedModelID"), 'unsupported2': new Device('EndDevice', '0x0017880104e45529', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", "notSupportedModelID"), 'interviewing': new Device('EndDevice', '0x0017880104e45530', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", undefined, true), @@ -119,10 +126,12 @@ const devices = { 'unsupported_router': new Device('Router', '0x0017880104e45525', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Mains (single phase)", "notSupportedModelID", false, "Boef"), 'CC2530_ROUTER': new Device('Router', '0x0017880104e45559', 6540,4151, [new Endpoint(1, [0, 6], [])], true, "Mains (single phase)", 'lumi.router'), 'LIVOLO': new Device('Router', '0x0017880104e45560', 6541,4152, [new Endpoint(6, [0, 6], [])], true, "Mains (single phase)", 'TI0001 '), + 'tradfri_remote': new Device('EndDevice', '0x90fd9ffffe4b64ae', 33906, 4476, [new Endpoint(1, [0], [0,3,4,6,8,5], '0x90fd9ffffe4b64ae')], true, "Battery", "TRADFRI remote control"), } const groups = { - 'group_1': new Group(1), + 'group_1': new Group(1, []), + 'group_tradfri_remote': new Group(15071, [bulb_color_2.endpoints[0], bulb_2.endpoints[0]]), } const mock = { @@ -153,7 +162,7 @@ const mock = { getPermitJoin: jest.fn().mockReturnValue(false), reset: jest.fn(), createGroup: jest.fn().mockImplementation((groupID) => { - const group = new Group(groupID); + const group = new Group(groupID, []); groups[`group_${groupID}`] = group return group; }) @@ -163,6 +172,7 @@ const mockConstructor = jest.fn().mockImplementation(() => mock); jest.mock('zigbee-herdsman', () => ({ Controller: mockConstructor, + Zcl: {ManufacturerCode: {Philips: 4107}}, })); module.exports = { From 9b67ed5bb6bb2e25cf0d24c14190efd2d925c980 Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Thu, 24 Oct 2019 18:19:37 +0200 Subject: [PATCH 4/5] Update herdsman. --- npm-shrinkwrap.json | 12 ++++++------ package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 28a9cfbd5a..3925fbe70d 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2717,9 +2717,9 @@ "dev": true }, "graceful-fs": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", - "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", "dev": true }, "growly": { @@ -6053,9 +6053,9 @@ } }, "zigbee-herdsman": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/zigbee-herdsman/-/zigbee-herdsman-0.11.0.tgz", - "integrity": "sha512-T14/9zWUk88kEMzbjQTV1/qZEWXG6ZE8EQZVgcpqlol8KQIICj5BblR0g9VNuRmI6Q4vy4eGUSUVVlVy5R2wLg==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/zigbee-herdsman/-/zigbee-herdsman-0.11.1.tgz", + "integrity": "sha512-eBIEZN8cF9hlPMRDUQ8J0LBEa7obYX74pmMwvK3dBmeMfc4m3kzNYZ0Lgc+XV93VSEEe9Hj9flOuHa93KUZKIQ==", "requires": { "debug": "^4.1.1", "fast-deep-equal": "^2.0.1", diff --git a/package.json b/package.json index 9e84d95959..eaaca4e1a7 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "rimraf": "*", "semver": "*", "winston": "*", - "zigbee-herdsman": "0.11.0", + "zigbee-herdsman": "0.11.1", "zigbee-herdsman-converters": "11.1.28" }, "devDependencies": { From 25d9006b5249f79c897ec0b9dc0286f63cdb3e2e Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Thu, 24 Oct 2019 22:12:21 +0200 Subject: [PATCH 5/5] Improve test stability. --- test/bridgeConfig.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/bridgeConfig.test.js b/test/bridgeConfig.test.js index f0485e3af1..95a2df6cc9 100644 --- a/test/bridgeConfig.test.js +++ b/test/bridgeConfig.test.js @@ -237,7 +237,6 @@ describe('Bridge config', () => { expect(device.removeFromNetwork).toHaveBeenCalledTimes(1); expect(controller.state[device.ieeeAddr]).toBeUndefined(); expect(settings.getDevice('bulb_color')).toBeNull(); - expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/log', JSON.stringify({type: 'device_removed', message: 'bulb_color'}),