From d4f01e6893fab56cf27e09bdb5889b6249cf60e4 Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Thu, 19 Sep 2019 20:36:05 +0200 Subject: [PATCH] Replace Livolo extension with DeviceEvent. https://github.com/Koenkk/zigbee2mqtt/issues/592 --- lib/controller.js | 11 +- lib/extension/baseExtension.js | 9 +- lib/extension/deviceEvent.js | 34 ++++++ lib/extension/livolo.js | 185 --------------------------------- lib/util/settings.js | 1 - npm-shrinkwrap.json | 12 +-- package.json | 4 +- test/deviceEvent.test.js | 62 +++++++++++ test/stub/data.js | 4 + test/stub/zigbeeHerdsman.js | 1 + 10 files changed, 116 insertions(+), 207 deletions(-) create mode 100644 lib/extension/deviceEvent.js delete mode 100644 lib/extension/livolo.js create mode 100644 test/deviceEvent.test.js diff --git a/lib/controller.js b/lib/controller.js index ac18f700..ee4f8030 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -19,7 +19,7 @@ const ExtensionGroups = require('./extension/groups'); const ExtensionDeviceAvailability = require('./extension/deviceAvailability'); const ExtensionDeviceBind = require('./extension/deviceBind'); const ExtensionDeviceReport = require('./extension/deviceReport'); -// const ExtensionLivolo = require('./extension/livolo'); +const ExtensionDeviceEvent = require('./extension/deviceEvent'); class Controller { constructor() { @@ -40,6 +40,7 @@ class Controller { new ExtensionBridgeConfig(this.zigbee, this.mqtt, this.state, this.publishEntityState), new ExtensionGroups(this.zigbee, this.mqtt, this.state, this.publishEntityState), new ExtensionDeviceBind(this.zigbee, this.mqtt, this.state, this.publishEntityState), + new ExtensionDeviceEvent(this.zigbee, this.mqtt, this.state, this.publishEntityState), ]; if (settings.get().advanced.report) { @@ -66,14 +67,6 @@ class Controller { this.zigbee, this.mqtt, this.state, this.publishEntityState )); } - - // TODO - // if (settings.get().experimental.livolo) { - // // https://github.com/Koenkk/zigbee2mqtt/issues/592 - // this.extensions.push(new ExtensionLivolo( - // this.zigbee, this.mqtt, this.state, this.publishEntityState - // )); - // } } async start() { diff --git a/lib/extension/baseExtension.js b/lib/extension/baseExtension.js index 5891eed3..77b77a65 100644 --- a/lib/extension/baseExtension.js +++ b/lib/extension/baseExtension.js @@ -26,11 +26,12 @@ class BaseExtension { /** * Is called when a Zigbee message from a device is received. - * @param {Object?} message The received message (can be null) - * @param {Object?} device The device of the message (can be null) - * @param {Object?} mappedDevice The mapped device (can be null) + * @param {string} type Type of the message + * @param {Object} data Data of the message + * @param {Object?} mappedDevice The mapped device + * @param {Object?} settingsDevice Device settings */ - // onZigbeeMessage(message, device, mappedDevice) {} + // onZigbeeEvent(type, data, mappedDevice, settingsDevice) {} /** * Is called when a MQTT message is received diff --git a/lib/extension/deviceEvent.js b/lib/extension/deviceEvent.js new file mode 100644 index 00000000..9df88982 --- /dev/null +++ b/lib/extension/deviceEvent.js @@ -0,0 +1,34 @@ +const BaseExtension = require('./baseExtension'); +const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); + +class DeviceEvent extends BaseExtension { + async onZigbeeStarted() { + for (const device of await this.zigbee.getClients()) { + this.callOnEvent(device, 'start', {}); + } + } + + onZigbeeEvent(type, data, mappedDevice, settingsDevice) { + if (data.device) { + this.callOnEvent(data.device, type, data, mappedDevice); + } + } + + async stop() { + for (const device of await this.zigbee.getClients()) { + this.callOnEvent(device, 'stop', {}); + } + } + + callOnEvent(device, type, data, mappedDevice) { + if (!mappedDevice) { + mappedDevice = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID); + } + + if (mappedDevice && mappedDevice.onEvent) { + mappedDevice.onEvent(type, data, device); + } + } +} + +module.exports = DeviceEvent; diff --git a/lib/extension/livolo.js b/lib/extension/livolo.js deleted file mode 100644 index e5328506..00000000 --- a/lib/extension/livolo.js +++ /dev/null @@ -1,185 +0,0 @@ -/* istanbul ignore file */ -// todo -const utils = require('../util/utils'); -const interval = utils.secondsToMilliseconds(1); -const logger = require('../util/logger'); -const BaseExtension = require('./baseExtension'); - -const foundationCfg = {manufSpec: 0, disDefaultRsp: 0}; - -/** - * Extension required for Livolo device support. - */ -class Livolo extends BaseExtension { - constructor(zigbee, mqtt, state, publishEntityState) { - super(zigbee, mqtt, state, publishEntityState); - this.timer = null; - this.configured = {}; - } - - onZigbeeStarted() { - this.startTimer(); - } - - _resetDeviceState(ieeeAddr) { - this.configured[ieeeAddr] = { - stage: 0, - retry: 0, - waitresp: false, - }; - } - - onZigbeeEvent(type, data, mappedDevice, settingsDevice) { - if (!data.device) { - return; - } - - if ((type == 'deviceInterview') || - (type == 'deviceJoined') || - (type == 'deviceAnnounce')) { - if (this.configured.hasOwnProperty(data.device.ieeeAddr)) { - logger.info(`LIVOLO ${data.device.ieeeAddr}. (Re)joins in the network (after power off?)`); - this._resetDeviceState(data.device.ieeeAddr); - } - return; - } - } - - startTimer() { - this.clearTimer(); - this.timer = setInterval(() => this.handleInterval(), interval); - } - - clearTimer() { - if (this.timer) { - clearTimeout(this.timer); - this.timer = null; - } - } - - stop() { - this.clearTimer(); - } - - _handleCommandRespSimple(err, rsp) { - this.ext.configured[this.ieeeAddr].waitresp = false; - - if (err) { - if (this.ctype === 'toggle') { - logger.debug(`LIVOLO ${this.ieeeAddr}. Toggle command error:`, err.message); - } - } else { - logger.debug(`LIVOLO ${this.ieeeAddr}. Sucessfully configured`, rsp); - this.ext.configured[this.ieeeAddr].stage = 1; // sucessfully send command - this.ext.configured[this.ieeeAddr].retry = 0; - this.device.status = 'online'; - } - } - - _handleCommandRespWithData(err, rsp) { - this.ext.configured[this.ieeeAddr].waitresp = false; - - if (err) { - logger.info(`LIVOLO ${this.ieeeAddr}. ${this.cid}.${this.ctype} response error:`, err.message); - if (this.ext.configured[this.ieeeAddr].retry >= 3) { - // errors in three sequental reads, stop polling, wait for a device message - this.device.status = 'offline'; - logger.info(`LIVOLO ${this.ieeeAddr}. Stopped polling after 3 unsuccessful attempts`); - } - } else { - this.ext.configured[this.ieeeAddr].retry = 0; - this.device.status = 'online'; - if (this.ext.zigbee) { - this.ext.zigbee.shepherd.emit('ind:reported', this.ep, this.cid, rsp, this.ep.last_af_msg); - } - } - } - - // msg: { groupid, clusterid, srcaddr, srcendpoint, dstendpoint, wasbroadcast, - // linkquality, securityuse, timestamp, transseqnumber, len, data } - _handleAfMessage(msg, ep) { - ep.linkquality = msg.linkquality; - ep.last_af_msg = msg; - } - - _sendToggle(zdev, ieeeAddr, ep, retry) { - this.zigbee.queue.push(ieeeAddr, (queueCallback) => { - const cfg = {}; - logger.debug(`LIVOLO ${ieeeAddr}. Sending the 'toggle' command. Retry: ${retry}`); - ep.functional('genOnOff', 'toggle', [cfg], foundationCfg, - this._handleCommandRespSimple.bind({ - device: zdev, - ieeeAddr, - cid: 'genOnOff', - ctype: 'toggle', - ext: this, - })); - - queueCallback(); - }); - } - - _sendPoll(zdev, ieeeAddr, ep, retry) { - this.zigbee.queue.push(ieeeAddr, (queueCallback) => { - ep.foundation('genOnOff', 'read', [{ - attrId: 0, // onOff - }], this._handleCommandRespWithData.bind({ - device: zdev, - ieeeAddr, - ep, - cid: 'genOnOff', - ctype: 'read', - ext: this, - })); - - queueCallback(); - }); - } - - async handleInterval() { - (await this.zigbee.getAllClients()) - .filter((d) => d.manufacturerName && d.manufacturerName.startsWith('LIVOLO')) // LIVOLO - .filter((d) => d.type === 'EndDevice') // Filter end devices - .filter((d) => d.powerSource && d.powerSource !== 'Battery') // Remove battery powered devices - .forEach((d) => { - const zdev = this.zigbee.shepherd._findDevByAddr(d.ieeeAddr); - if (zdev && zdev.endpoints) { - const eplist = Object.keys(zdev.endpoints).filter((epId) => { - const ep2 = zdev.getEndpoint(epId); - const clist = ep2.getClusterList(); - return clist && clist.includes(6); // 6 - genOnOff - }); - - if (eplist.length > 0) { - const ep = zdev.getEndpoint(eplist[0]); - - ep.onAfIncomingMsg = this._handleAfMessage; - - if (!this.configured.hasOwnProperty(d.ieeeAddr)) { - this._resetDeviceState(d.ieeeAddr); - } - - const state = this.configured[d.ieeeAddr]; - if (state.waitresp) { - return; - } - - if (state.retry < 3) { - if (state.stage === 0) { - state.retry += 1; - state.waitresp = true; - this._sendToggle(zdev, d.ieeeAddr, ep, state.retry); - } else if (state.stage === 1) { - state.retry += 1; - state.waitresp = true; - this._sendPoll(zdev, d.ieeeAddr, ep, state.retry); - } - } - } - } - }); - return true; - } -} - -module.exports = Livolo; diff --git a/lib/util/settings.js b/lib/util/settings.js index 9da3e969..d2a4b728 100644 --- a/lib/util/settings.js +++ b/lib/util/settings.js @@ -36,7 +36,6 @@ const defaults = { }, }, experimental: { - livolo: false, // json or attribute output: 'json', }, diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index cdae9ba1..8d9cec7b 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -5938,9 +5938,9 @@ } }, "zigbee-herdsman": { - "version": "0.6.13", - "resolved": "https://registry.npmjs.org/zigbee-herdsman/-/zigbee-herdsman-0.6.13.tgz", - "integrity": "sha512-Tcy+yaJ25+ocOadXOicUIUQLZFGDmRf9Y+yUfG5fYtap738l0BDaKd/hdVjhanOB5AQXYtQvAvMNhdn7szfOEg==", + "version": "0.6.14", + "resolved": "https://registry.npmjs.org/zigbee-herdsman/-/zigbee-herdsman-0.6.14.tgz", + "integrity": "sha512-QQNqSf+TkAVxdy6fxcqPbd7Jx+if03GTyjMjUK7ajZotYhweZQQaXMI3uATXggrBAd88YpbnCaUxaKkGMsC1Dw==", "requires": { "debug": "^4.1.1", "fast-deep-equal": "^2.0.1", @@ -12974,9 +12974,9 @@ } }, "zigbee-herdsman-converters": { - "version": "11.0.10", - "resolved": "https://registry.npmjs.org/zigbee-herdsman-converters/-/zigbee-herdsman-converters-11.0.10.tgz", - "integrity": "sha512-ENNqj5crUHvLCV3AJ/Azqn/+nZ+uqEvKmJ1x0LV9+Cv1X/7hdzhoKYC/HGrr9DLpuh1TavvP4VHU5COpUTZJRg==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/zigbee-herdsman-converters/-/zigbee-herdsman-converters-11.1.0.tgz", + "integrity": "sha512-yNW2IUjKmCH3186onxfshSiUDC2Ye9dwmxnJo0/09bix+U86/7SyfJGphVvV9dWwjwjbGPwm/wH942PLOchrAQ==", "requires": { "debounce": "^1.2.0" } diff --git a/package.json b/package.json index 749743c9..adda6df0 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,8 @@ "rimraf": "*", "semver": "*", "winston": "2.4.2", - "zigbee-herdsman": "0.6.13", - "zigbee-herdsman-converters": "11.0.10" + "zigbee-herdsman": "0.6.14", + "zigbee-herdsman-converters": "11.1.0" }, "devDependencies": { "eslint": "*", diff --git a/test/deviceEvent.test.js b/test/deviceEvent.test.js new file mode 100644 index 00000000..5b5333a6 --- /dev/null +++ b/test/deviceEvent.test.js @@ -0,0 +1,62 @@ +const data = require('./stub/data'); +const logger = require('./stub/logger'); +const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); +zigbeeHerdsman.returnDevices.push('0x00124b00120144ae'); +zigbeeHerdsman.returnDevices.push('0x0017880104e45560'); +const MQTT = require('./stub/mqtt'); +const settings = require('../lib/util/settings'); +const Controller = require('../lib/controller'); +const flushPromises = () => new Promise(setImmediate); +const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); + +const mocksClear = [MQTT.publish, logger.warn, logger.debug]; + +const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); +const mockOnEvent = jest.fn(); +const mappedLivolo = zigbeeHerdsmanConverters.findByZigbeeModel(zigbeeHerdsman.devices.LIVOLO.modelID); +mappedLivolo.onEvent = mockOnEvent; + +describe('onlythis Device event', () => { + let controller; + const device = zigbeeHerdsman.devices.LIVOLO; + + beforeEach(async () => { + data.writeDefaultConfiguration(); + settings._reRead(); + data.writeEmptyState(); + controller = new Controller(); + await controller.start(); + mocksClear.forEach((m) => m.mockClear()); + await flushPromises(); + }); + + it('Should call with start event', async () => { + expect(mockOnEvent).toHaveBeenCalledTimes(1); + const call = mockOnEvent.mock.calls[0]; + expect(call[0]).toBe('start') + expect(call[1]).toStrictEqual({}) + expect(call[2]).toBe(device); + }); + + it('Should call with stop event', async () => { + mockOnEvent.mockClear(); + await controller.stop(); + await flushPromises(); + expect(mockOnEvent).toHaveBeenCalledTimes(1); + const call = mockOnEvent.mock.calls[0]; + expect(call[0]).toBe('stop') + expect(call[1]).toStrictEqual({}) + expect(call[2]).toBe(device); + }); + + it('Should call with zigbee event', async () => { + mockOnEvent.mockClear(); + await zigbeeHerdsman.events.deviceAnnounce({device}); + await flushPromises(); + expect(mockOnEvent).toHaveBeenCalledTimes(1); + const call = mockOnEvent.mock.calls[0]; + expect(call[0]).toBe('deviceAnnounce') + expect(call[1]).toStrictEqual({device}) + expect(call[2]).toBe(device); + }); +}); diff --git a/test/stub/data.js b/test/stub/data.js index 6df99aa5..d21cff7a 100644 --- a/test/stub/data.js +++ b/test/stub/data.js @@ -113,6 +113,10 @@ function writeDefaultConfiguration() { '0x0017880104e45559': { retain: false, friendly_name: 'cc2530_router' + }, + '0x0017880104e45560': { + retain: false, + friendly_name: 'livolo' } }, groups: { diff --git a/test/stub/zigbeeHerdsman.js b/test/stub/zigbeeHerdsman.js index 2b97fb86..0b326446 100644 --- a/test/stub/zigbeeHerdsman.js +++ b/test/stub/zigbeeHerdsman.js @@ -123,6 +123,7 @@ const devices = { 'nomodel': new Device('Router', '0x0017880104e45535', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Mains (single phase)", undefined, true), '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(1, [0, 6], [])], true, "Mains (single phase)", 'TI0001 '), } const groups = {