diff --git a/lib/controller.js b/lib/controller.js index 82c39c6d1..14b54fa14 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -169,63 +169,66 @@ class Controller { async onZigbeeEvent(type, data) { const entity = this.zigbee.resolveEntity(data.device || data.ieeeAddr); - if (!entity.settings && data.device) { + if (data.device && !entity.settings) { // Only deviceLeave doesn't have a device (not interesting to add to settings) entity.settings = settings.addDevice(data.device.ieeeAddr); } - const friendlyName = entity.settings.friendlyName; + const name = entity && entity.settings ? entity.settings.friendlyName : null; if (type === 'message') { logger.debug( - `Received Zigbee message from '${entity.settings.friendlyName}' of type '${data.type}' ` + + `Received Zigbee message from '${name}' of type '${data.type}' ` + `with data '${JSON.stringify(data.data)}' from endpoint ${data.endpoint.ID}` + (data.hasOwnProperty('groupID') ? ` with groupID ${data.groupID}` : ``) ); } else if (type === 'deviceJoined') { - logger.info(`Device '${friendlyName}' joined`); - this.mqtt.log('device_connected', {friendly_name: friendlyName}); + logger.info(`Device '${name}' joined`); + this.mqtt.log('device_connected', {friendly_name: name}); } else if (type === 'deviceInterview') { if (data.status === 'successful') { - logger.info(`Successfully interviewed '${friendlyName}', device has succesfully been paired`); + logger.info(`Successfully interviewed '${name}', device has succesfully been paired`); if (entity.mapped) { const {vendor, description, model} = entity.mapped; logger.info( - `Device '${friendlyName}' is supported, identified as: ${vendor} ${description} (${model})` + `Device '${name}' is supported, identified as: ${vendor} ${description} (${model})` ); - const log = {friendly_name: friendlyName, model, vendor, description, supported: true}; + const log = {friendly_name: name, model, vendor, description, supported: true}; this.mqtt.log('pairing', 'interview_successful', log); } else { logger.warn( - `Device '${friendlyName}' with Zigbee model '${data.device.modelID}' is NOT supported, ` + + `Device '${name}' with Zigbee model '${data.device.modelID}' is NOT supported, ` + `please follow https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html` ); - this.mqtt.log('pairing', 'interview_successful', {friendly_name: friendlyName, supported: false}); + this.mqtt.log('pairing', 'interview_successful', {friendly_name: name, supported: false}); } } else if (data.status === 'failed') { - logger.error(`Failed to interview '${friendlyName}', device has not succesfully been paired`); - this.mqtt.log('pairing', 'interview_failed', {friendly_name: friendlyName}); + logger.error(`Failed to interview '${name}', device has not succesfully been paired`); + this.mqtt.log('pairing', 'interview_failed', {friendly_name: name}); } else { /* istanbul ignore else */ if (data.status === 'started') { - logger.info(`Starting interview of '${friendlyName}'`); - this.mqtt.log('pairing', 'interview_started', {friendly_name: friendlyName}); + logger.info(`Starting interview of '${name}'`); + this.mqtt.log('pairing', 'interview_started', {friendly_name: name}); } } } else if (type === 'deviceAnnounce') { - logger.debug(`Device '${friendlyName}' announced itself`); + logger.debug(`Device '${name}' announced itself`); } else { /* istanbul ignore else */ if (type === 'deviceLeave') { - logger.warn(`Device '${friendlyName}' left the network`); - this.mqtt.log('device_removed', 'left_network', {friendly_name: friendlyName}); + logger.warn(`Device '${name || data.ieeeAddr}' left the network`); + this.mqtt.log('device_removed', 'left_network', {friendly_name: name || data.ieeeAddr}); } } // Call extensions - this.callExtensionMethod('onZigbeeEvent', [type, data, entity.mapped, entity.settings]); + this.callExtensionMethod( + 'onZigbeeEvent', + [type, data, entity ? entity.mapped : null, entity ? entity.settings : null] + ); } onMQTTMessage(payload) { diff --git a/lib/extension/bridgeConfig.js b/lib/extension/bridgeConfig.js index 73ded8eda..fa3efd65e 100644 --- a/lib/extension/bridgeConfig.js +++ b/lib/extension/bridgeConfig.js @@ -223,7 +223,7 @@ class BridgeConfig extends BaseExtension { } async removeOrBan(ban, message) { - const entity = this.zigbee.resolveEntity(message); + const entity = this.zigbee.resolveEntity(message.trim()); const cleanup = () => { // Remove from configuration.yaml diff --git a/lib/zigbee.js b/lib/zigbee.js index 5c9d975bf..3af4ba433 100644 --- a/lib/zigbee.js +++ b/lib/zigbee.js @@ -154,6 +154,10 @@ class Zigbee extends events.EventEmitter { return null; } else if (entity.type === 'device') { const device = this.getDeviceByIeeeAddr(entity.ID); + if (!device) { + return null; + } + const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID); const endpoints = mapped && mapped.endpoint ? mapped.endpoint(device) : null; let isDefaultEndpoint = true; diff --git a/test/controller.test.js b/test/controller.test.js index 00c2169f5..6f5f7bef8 100644 --- a/test/controller.test.js +++ b/test/controller.test.js @@ -20,6 +20,7 @@ describe('Controller', () => { let controller; beforeEach(() => { + zigbeeHerdsman.returnDevices.splice(0); controller = new Controller(); mocksClear.forEach((m) => m.mockClear()); data.writeDefaultConfiguration(); @@ -303,13 +304,27 @@ describe('Controller', () => { expect(logger.debug).toHaveBeenCalledWith(`Device 'bulb' announced itself`); }); - it('On zigbee event device leave', async () => { + it('On zigbee event device leave (removed from database and settings)', async () => { await controller.start(); + zigbeeHerdsman.returnDevices.push('0x00124b00120144ae'); + settings.set(['devices'], {}) + MQTT.publish.mockClear(); const device = zigbeeHerdsman.devices.bulb; const payload = {ieeeAddr: device.ieeeAddr}; await zigbeeHerdsman.events.deviceLeave(payload); await flushPromises(); - expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/log', '{"type":"device_removed","message":"left_network","meta":{"friendly_name":"bulb"}}', { retain: false, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/log', '{"type":"device_removed","message":"left_network","meta":{"friendly_name":"0x000b57fffec6a5b2"}}', { retain: false, qos: 0}, expect.any(Function)); + }); + + it('On zigbee event device leave (removed from database and NOT settings)', async () => { + await controller.start(); + zigbeeHerdsman.returnDevices.push('0x00124b00120144ae'); + const device = zigbeeHerdsman.devices.bulb; + MQTT.publish.mockClear(); + const payload = {ieeeAddr: device.ieeeAddr}; + await zigbeeHerdsman.events.deviceLeave(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/log', '{"type":"device_removed","message":"left_network","meta":{"friendly_name":"0x000b57fffec6a5b2"}}', { retain: false, qos: 0}, expect.any(Function)); }); it('Publish entity state attribute output', async () => {