From f1c785eedd7c57bee64008237b0e655cba2e9ebc Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Wed, 8 Jul 2020 21:46:00 +0200 Subject: [PATCH] Implement new api configure. https://github.com/Koenkk/zigbee2mqtt/issues/3281 --- lib/extension/configure.js | 50 ++++++++++++++++++++++++++++---------- test/configure.test.js | 50 +++++++++++++++++++++++++++++++++++--- 2 files changed, 84 insertions(+), 16 deletions(-) diff --git a/lib/extension/configure.js b/lib/extension/configure.js index f7a43679..bc6c9261 100644 --- a/lib/extension/configure.js +++ b/lib/extension/configure.js @@ -1,4 +1,5 @@ const settings = require('../util/settings'); +const utils = require('../util/utils'); const logger = require('../util/logger'); const Extension = require('./extension'); @@ -12,7 +13,7 @@ class Configure extends Extension { this.configuring = new Set(); this.attempts = {}; - this.legacyApi = settings.get().advanced.legacy_api; + this.topic = `${settings.get().mqtt.base_topic}/bridge/request/device/configure`; this.legacyTopic = `${settings.get().mqtt.base_topic}/bridge/configure`; } @@ -36,18 +37,18 @@ class Configure extends Extension { onMQTTConnected() { /* istanbul ignore else */ - if (this.legacyApi) { + if (settings.get().advanced.legacy_api) { this.mqtt.subscribe(this.legacyTopic); } + + /* istanbul ignore else */ + if (settings.get().experimental.new_api) { + this.mqtt.subscribe(this.topic); + } } async onMQTTMessage(topic, message) { - /* istanbul ignore else */ - if (this.legacyApi) { - if (topic !== this.legacyTopic) { - return; - } - + if (topic === this.legacyTopic) { const resolvedEntity = this.zigbee.resolveEntity(message); if (!resolvedEntity || resolvedEntity.type !== 'device') { logger.error(`Device '${message}' does not exist`); @@ -60,6 +61,26 @@ class Configure extends Extension { } this.configure(resolvedEntity, true); + } else if (topic === this.topic) { + message = utils.parseJSON(message, message); + const ID = typeof message === 'object' && message.hasOwnProperty('ID') ? message.ID : message; + let error = null; + + const resolvedEntity = this.zigbee.resolveEntity(ID); + if (!resolvedEntity || resolvedEntity.type !== 'device') { + error = `Device '${ID}' does not exist`; + } else if (!resolvedEntity.definition || !resolvedEntity.definition.configure) { + error = `Device '${resolvedEntity.name}' cannot be configured`; + } else { + try { + await this.configure(resolvedEntity, true, true); + } catch (e) { + error = `Failed to configure (${e.message})`; + } + } + + const response = utils.getResponse(message, {ID}, error); + await this.mqtt.publish(`bridge/response/device/configure`, JSON.stringify(response)); } } @@ -87,7 +108,7 @@ class Configure extends Extension { } } - async configure(resolvedEntity, force=false) { + async configure(resolvedEntity, force=false, thowError=false) { const device = resolvedEntity.device; if (this.configuring.has(device.ieeeAddr) || (this.attempts[device.ieeeAddr] >= 3 && !force)) { return false; @@ -106,11 +127,14 @@ class Configure extends Extension { device.meta.configured = resolvedEntity.definition.meta.configureKey; device.save(); } catch (error) { - logger.error( - `Failed to configure '${resolvedEntity.name}', ` + - `attempt ${this.attempts[device.ieeeAddr] + 1} (${error.stack})`, - ); this.attempts[device.ieeeAddr]++; + const attempt = this.attempts[device.ieeeAddr]; + const msg = `Failed to configure '${resolvedEntity.name}', attempt ${attempt} (${error.stack})`; + logger.error(msg); + + if (thowError) { + throw error; + } } this.configuring.delete(device.ieeeAddr); diff --git a/test/configure.test.js b/test/configure.test.js index 49f5f11c..95fe4b2c 100644 --- a/test/configure.test.js +++ b/test/configure.test.js @@ -90,8 +90,52 @@ describe('Configure', () => { expectRemoteConfigured(); }); - it('Should allow to reconfigure manually', async () => { + it('Should allow to configure via MQTT', async () => { expectRemoteConfigured(); + mockClear(zigbeeHerdsman.devices.remote); + expectRemoteNotConfigured(); + await MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', 'remote'); + await flushPromises(); + expectRemoteConfigured(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/configure', + JSON.stringify({"data":{"ID": "remote"},"status":"ok"}), + {retain: false, qos: 0}, expect.any(Function) + ); + }); + + it('Fail to configure via MQTT when device does not exist', async () => { + await MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', JSON.stringify({ID: "not_existing_device"})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/configure', + JSON.stringify({"data":{"ID": "not_existing_device"},"status":"error","error": "Device 'not_existing_device' does not exist"}), + {retain: false, qos: 0}, expect.any(Function) + ); + }); + + it('Fail to configure via MQTT when configure fails', async () => { + zigbeeHerdsman.devices.remote.getEndpoint(1).bind.mockImplementationOnce(async () => {throw new Error('Bind timeout after 10s')}); + await MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', JSON.stringify({ID: "remote"})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/configure', + JSON.stringify({"data":{"ID": "remote"},"status":"error","error": "Failed to configure (Bind timeout after 10s)"}), + {retain: false, qos: 0}, expect.any(Function) + ); + }); + + it('Fail to configure via MQTT when device has no configure', async () => { + await MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', JSON.stringify({ID: "bulb", transaction: 20})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/configure', + JSON.stringify({"data":{"ID": "bulb"},"status":"error","error": "Device 'bulb' cannot be configured","transaction":20}), + {retain: false, qos: 0}, expect.any(Function) + ); + }); + + it('Legacy api: Should allow to reconfigure manually', async () => { mockClear(zigbeeHerdsman.devices.remote); expectRemoteNotConfigured(); await MQTT.events.message('zigbee2mqtt/bridge/configure', 'remote'); @@ -99,13 +143,13 @@ describe('Configure', () => { expectRemoteConfigured(); }); - it('Shouldnt manually reconfigure when device does not exist', async () => { + it('Legacy api: Shouldnt manually reconfigure when device does not exist', async () => { await MQTT.events.message('zigbee2mqtt/bridge/configure', 'remote_random_non_existing'); await flushPromises(); expect(logger.error).toHaveBeenCalledWith(`Device 'remote_random_non_existing' does not exist`); }); - it('Should skip reconfigure when device does not require this', async () => { + it('Legacy api: Should skip reconfigure when device does not require this', async () => { await MQTT.events.message('zigbee2mqtt/bridge/configure', 'bulb'); await flushPromises(); expect(logger.warn).toHaveBeenCalledWith(`Skipping configure of 'bulb', device does not require this.`)