diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index db6b4be107..40f2130bcc 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -40,6 +40,7 @@ export default class Bridge extends Extension { 'device/options': this.deviceOptions, 'device/configure_reporting': this.deviceConfigureReporting, 'device/remove': this.deviceRemove, + 'device/interview': this.deviceInterview, 'device/generate_external_definition': this.deviceGenerateExternalDefinition, 'device/rename': this.deviceRename, 'group/add': this.groupAdd, @@ -500,6 +501,25 @@ export default class Bridge extends Extension { }, null); } + @bind async deviceInterview(message: string | KeyValue): Promise { + if (typeof message !== 'object' || !message.hasOwnProperty('id')) { + throw new Error(`Invalid payload`); + } + + const device = this.getEntity('device', message.id) as Device; + + try { + await device.zh.interview(); + } catch (error) { + throw new Error(`interview of '${device.name}' (${device.ieeeAddr}) failed: ${error}`, {cause: error}); + } + + // publish devices so that the front-end has up-to-date info. + await this.publishDevices(); + + return utils.getResponse(message, {id: message.id}, null); + } + @bind async deviceGenerateExternalDefinition(message: string | KeyValue): Promise { if (typeof message !== 'object' || !message.hasOwnProperty('id')) { throw new Error(`Invalid payload`); diff --git a/test/bridge.test.js b/test/bridge.test.js index c66d7fefd1..f50617224e 100644 --- a/test/bridge.test.js +++ b/test/bridge.test.js @@ -66,6 +66,7 @@ describe('Bridge', () => { logger.setTransportsEnabled(false); MQTT.publish.mockClear(); const device = zigbeeHerdsman.devices.bulb; + device.interview.mockClear(); device.removeFromDatabase.mockClear(); device.removeFromNetwork.mockClear(); extension.lastJoinedDeviceIeeeAddr = null; @@ -727,6 +728,103 @@ describe('Bridge', () => { ); }); + it('Should allow interviewing a device by friendly name', async () => { + MQTT.publish.mockClear(); + zigbeeHerdsman.devices.bulb.interview.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb'})); + await flushPromises(); + expect(zigbeeHerdsman.devices.bulb.interview).toHaveBeenCalled(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/interview', + stringify({"data":{"id":"bulb"},"status":"ok"}), + {retain: false, qos: 0}, expect.any(Function) + ); + + // The following indicates that devices have published. + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/devices', + expect.any(String), + {retain: true, qos: 0}, expect.any(Function) + ); + }); + + it('Should allow interviewing a device by ieeeAddr', async () => { + MQTT.publish.mockClear(); + zigbeeHerdsman.devices.bulb.interview.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: "0x000b57fffec6a5b2"})); + await flushPromises(); + expect(zigbeeHerdsman.devices.bulb.interview).toHaveBeenCalled(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/interview', + stringify({"data":{"id":"0x000b57fffec6a5b2"},"status":"ok"}), + {retain: false, qos: 0}, expect.any(Function) + ); + + // The following indicates that devices have published. + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/devices', + expect.any(String), + {retain: true, qos: 0}, expect.any(Function) + ); + }); + + it('Should throw error on invalid device interview payload', async () => { + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({foo: 'bulb'})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/interview', + stringify({"data":{},"status":"error","error":"Invalid payload"}), + {retain: false, qos: 0}, expect.any(Function) + ); + }); + + it('Should throw error on non-existing device interview', async () => { + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb_not_existing'})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/interview', + stringify({"data":{},"status":"error","error":"Device 'bulb_not_existing' does not exist"}), + {retain: false, qos: 0}, expect.any(Function) + ); + }); + + it('Should throw error on id is device endpoint', async () => { + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb/1'})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/interview', + stringify({"data":{},"status":"error","error":"Device 'bulb/1' does not exist"}), + {retain: false, qos: 0}, expect.any(Function) + ); + }); + + it('Should throw error on id is a group', async () => { + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'group_1'})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/interview', + stringify({"data":{},"status":"error","error":"Device 'group_1' does not exist"}), + {retain: false, qos: 0}, expect.any(Function) + ); + }); + + it('Should throw error on when interview fails', async () => { + MQTT.publish.mockClear(); + zigbeeHerdsman.devices.bulb.interview.mockClear(); + zigbeeHerdsman.devices.bulb.interview.mockImplementation(() => Promise.reject(new Error('something went wrong'))) + MQTT.events.message('zigbee2mqtt/bridge/request/device/interview', stringify({id: 'bulb'})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/interview', + stringify({"data":{},"status":"error","error":"interview of 'bulb' (0x000b57fffec6a5b2) failed: Error: something went wrong"}), + {retain: false, qos: 0}, expect.any(Function) + ); + }); + it('Should error when generate_external_definition is invalid', async () => { MQTT.publish.mockClear(); MQTT.events.message('zigbee2mqtt/bridge/request/device/generate_external_definition', stringify({wrong: ZNCZ02LM.ieeeAddr})); diff --git a/test/stub/zigbeeHerdsman.js b/test/stub/zigbeeHerdsman.js index 5ccab350aa..7f779b2f3b 100644 --- a/test/stub/zigbeeHerdsman.js +++ b/test/stub/zigbeeHerdsman.js @@ -127,6 +127,7 @@ class Device { this.softwareBuildID = softwareBuildID; this.interviewCompleted = interviewCompleted; this.modelID = modelID; + this.interview = jest.fn(); this.interviewing = interviewing; this.meta = {}; this.ping = jest.fn();