From 9b7ecff5d04fec941f6ee0260755287d7005788e Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Mon, 28 Dec 2020 20:19:00 +0100 Subject: [PATCH] Clear outdated availability topics. #5414 --- lib/extension/availability.js | 14 +++++++++++++ lib/extension/bridge.js | 7 ++++--- lib/extension/legacy/bridgeLegacy.js | 2 +- test/availability.test.js | 31 ++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/lib/extension/availability.js b/lib/extension/availability.js index 7016c4f0..cdcbec77 100644 --- a/lib/extension/availability.js +++ b/lib/extension/availability.js @@ -3,6 +3,7 @@ const settings = require('../util/settings'); const utils = require('../util/utils'); const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); const Extension = require('./extension'); +const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/(.*)/availability`); // Pingable end devices, some end devices should be pinged // e.g. E11-G13 https://github.com/Koenkk/zigbee2mqtt/issues/775#issuecomment-453683846 @@ -25,6 +26,7 @@ class Availability extends Extension { this.state = {}; this.eventBus.on('deviceRemoved', (data) => this.onDeviceRemoved(data.resolvedEntity), this.constructor.name); + this.eventBus.on('deviceRenamed', (data) => this.onDeviceRenamed(data), this.constructor.name); this.blocklist = settings.get().advanced.availability_blocklist .concat(settings.get().advanced.availability_blacklist) @@ -35,6 +37,10 @@ class Availability extends Extension { .map((e) => settings.getEntity(e).ID); } + onDeviceRenamed(data) { + this.mqtt.publish(`${data.from}/availability`, null, {retain: true, qos: 0}); + } + onDeviceRemoved(resolvedEntity) { this.mqtt.publish(`${resolvedEntity.name}/availability`, null, {retain: true, qos: 0}); delete this.state[resolvedEntity.device.ieeeAddr]; @@ -86,6 +92,14 @@ class Availability extends Extension { } } + async onMQTTMessage(topic, message) { + // Clear topics for non-existing devices + const match = topic.match(topicRegex); + if (match && (!this.zigbee.resolveEntity(match[1]) || this.zigbee.resolveEntity(match[1]).name !== match[1])) { + this.mqtt.publish(`${match[1]}/availability`, null, {retain: true, qos: 0}); + } + } + async handleIntervalPingable(device) { // When a device is already unavailable, log the ping failed on 'debug' instead of 'error'. const resolvedEntity = this.zigbee.resolveEntity(device.ieeeAddr); diff --git a/lib/extension/bridge.js b/lib/extension/bridge.js index 431f49b5..f053bcb2 100644 --- a/lib/extension/bridge.js +++ b/lib/extension/bridge.js @@ -337,12 +337,13 @@ class Bridge extends Extension { // Clear retained messages this.mqtt.publish(entity.name, '', {retain: true}); + const oldFriendlyName = entity.settings.friendlyName; if (entity.type === 'device') { this.publishDevices(); - this.eventBus.emit(`deviceRenamed`, {device: entity.device, homeAssisantRename}); + this.eventBus.emit(`deviceRenamed`, {device: entity.device, homeAssisantRename, from: oldFriendlyName, to}); } else { this.publishGroups(); - this.eventBus.emit(`groupRenamed`, {group: entity.group, homeAssisantRename}); + this.eventBus.emit(`groupRenamed`, {group: entity.group, homeAssisantRename, from: oldFriendlyName, to}); } // Repulish entity state @@ -350,7 +351,7 @@ class Bridge extends Extension { return utils.getResponse( message, - {from: entity.settings.friendlyName, to, homeassistant_rename: homeAssisantRename}, + {from: oldFriendlyName, to, homeassistant_rename: homeAssisantRename}, null, ); } diff --git a/lib/extension/legacy/bridgeLegacy.js b/lib/extension/legacy/bridgeLegacy.js index 9fe5f878..20b018cb 100644 --- a/lib/extension/legacy/bridgeLegacy.js +++ b/lib/extension/legacy/bridgeLegacy.js @@ -231,7 +231,7 @@ class BridgeLegacy extends Extension { settings.changeFriendlyName(from, to); logger.info(`Successfully renamed - ${from} to ${to} `); const entity = this.zigbee.resolveEntity(to); - const eventData = isGroup ? {group: entity.group} : {device: entity.device}; + const eventData = isGroup ? {group: entity.group} : {device: entity.device, from, to}; eventData.homeAssisantRename = false; this.eventBus.emit(`${isGroup ? 'group' : 'device'}Renamed`, eventData); diff --git a/test/availability.test.js b/test/availability.test.js index eed8add2..a0293a12 100644 --- a/test/availability.test.js +++ b/test/availability.test.js @@ -1,5 +1,6 @@ const data = require('./stub/data'); const logger = require('./stub/logger'); +const stringify = require('json-stable-stringify-without-jsonify'); const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b3'); zigbeeHerdsman.returnDevices.push('0x00124b00120144ae'); @@ -408,4 +409,34 @@ describe('Availability', () => { {retain: true, qos: 0}, expect.any(Function) ); }); + + it('Should clear retained availability topic when device is renamed', async () => { + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/request/device/rename', stringify({"from": "bulb_color", "to": "bulb_color_new_name"})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color/availability', + null, + {retain: true, qos: 0}, expect.any(Function) + ); + }); + + it('Should clear retained availability topic when device does not exist', async () => { + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/not_existing_hahaha/availability', 'offline'); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/not_existing_hahaha/availability', null, {retain: true, qos: 0}, expect.any(Function)); + + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/0x000b57fffec6a5b3/availability', 'offline'); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/0x000b57fffec6a5b3/availability', null, {retain: true, qos: 0}, expect.any(Function)); + + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bulb_color/availability', 'offline'); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(0); + }); });