From 5f75c9c9f87ae8d75bb092a67b25c2497e353f06 Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Mon, 17 Aug 2020 17:24:57 +0200 Subject: [PATCH] Clear outdated Home Assistant configs --- lib/extension/homeassistant.js | 37 +++++++++++++++++++++++++++++---- test/homeassistant.test.js | 38 ++++++++++++++++++++++++++++++++++ test/publish.test.js | 2 +- 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/lib/extension/homeassistant.js b/lib/extension/homeassistant.js index 2dbba689..bad83d98 100644 --- a/lib/extension/homeassistant.js +++ b/lib/extension/homeassistant.js @@ -6,6 +6,7 @@ const zigbee2mqttVersion = require('../../package.json').version; const Extension = require('./extension'); const objectAssignDeep = require(`object-assign-deep`); const stringify = require('json-stable-stringify'); +const discoveryRegex = new RegExp(`homeassistant/(.*)/(.*)/(.*)/config`); const cfg = { // Binary sensor @@ -2049,6 +2050,7 @@ class HomeAssistant extends Extension { async onMQTTConnected() { this.mqtt.subscribe(this.statusTopic); this.mqtt.subscribe(defaultStatusTopic); + this.mqtt.subscribe(`${this.discoveryTopic}/#`); // MQTT discovery of all paired devices on startup. for (const device of this.zigbee.getClients()) { @@ -2252,11 +2254,38 @@ class HomeAssistant extends Extension { } onMQTTMessage(topic, message) { - if (topic !== this.statusTopic && topic !== defaultStatusTopic) { - return false; - } + const discoveryMatch = topic.match(discoveryRegex); + if (discoveryMatch) { + // Clear outdated discovery configs. + try { + message = JSON.parse(message); + if (!message || !message.availability_topic || + !message.availability_topic.startsWith(settings.get().mqtt.base_topic + '/')) { + // Base topic is different, probably different Zigbee2MQTT instance. + return; + } + } catch (e) { + return; + } - if (message.toLowerCase() === 'online') { + const ieeeAddr = discoveryMatch[2]; + const resolvedEntity = this.zigbee.resolveEntity(ieeeAddr); + let clear = !resolvedEntity || !resolvedEntity.definition; + + if (!clear) { + const deviceSettings = {...settings.get().device_options, ...resolvedEntity.settings}; + const configs = this.getConfigs(resolvedEntity.definition, deviceSettings); + const type = discoveryMatch[1]; + const objectID = discoveryMatch[3]; + clear = !configs.find((c) => c.type === type && c.object_id === objectID); + } + + if (clear) { + logger.debug(`Clearing Home Assistant config '${topic}'`); + topic = topic.substring(this.discoveryTopic.length + 1); + this.mqtt.publish(topic, null, {retain: true, qos: 0}, this.discoveryTopic); + } + } else if ((topic === this.statusTopic || topic === defaultStatusTopic) && message.toLowerCase() === 'online') { const timer = setTimeout(async () => { // Publish all device states. for (const device of this.zigbee.getClients()) { diff --git a/test/homeassistant.test.js b/test/homeassistant.test.js index 82070cf8..66cfe3cf 100644 --- a/test/homeassistant.test.js +++ b/test/homeassistant.test.js @@ -1033,4 +1033,42 @@ describe('HomeAssistant extension', () => { }; expect(ha._getMapping()['external_converters_device']).toStrictEqual([homeassistantSwitch]); }); + + it('onlythis Should clear outdated configs', async () => { + controller = new Controller(false); + await controller.start(); + await flushPromises(); + + // Non-existing device -> clear + MQTT.publish.mockClear(); + await MQTT.events.message('homeassistant/sensor/0x123/temperature/config', stringify({availability_topic: 'zigbee2mqtt/0x123/availability'})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish).toHaveBeenCalledWith('homeassistant/sensor/0x123/temperature/config', null, {qos: 0, retain: true}, expect.any(Function)); + + // Existing device -> don't clear + MQTT.publish.mockClear(); + await MQTT.events.message('homeassistant/binary_sensor/0x000b57fffec6a5b2/update_available/config', stringify({availability_topic: 'zigbee2mqtt/0x000b57fffec6a5b2/availability'})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(0); + + // Non-existing device of different instance -> don't clear + MQTT.publish.mockClear(); + await MQTT.events.message('homeassistant/sensor/0x123/temperature/config', stringify({availability_topic: 'zigbee2mqtt_different/0x123/availability'})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(0); + + // Existing device but non-existing config -> don't clear + MQTT.publish.mockClear(); + await MQTT.events.message('homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', stringify({availability_topic: 'zigbee2mqtt/0x000b57fffec6a5b2/availability'})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish).toHaveBeenCalledWith('homeassistant/sensor/0x000b57fffec6a5b2/update_available/config', null, {qos: 0, retain: true}, expect.any(Function)); + + // Non-existing device but invalid payload -> clear + MQTT.publish.mockClear(); + await MQTT.events.message('homeassistant/sensor/0x123/temperature/config', '1}3'); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(0); + }); }); diff --git a/test/publish.test.js b/test/publish.test.js index 1d94318b..362f9b6b 100644 --- a/test/publish.test.js +++ b/test/publish.test.js @@ -972,7 +972,7 @@ describe('Publish', () => { expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', color: {x: 0.280632719756407, y: 0.288286029784579}, color_temp: 100}); }); - it('onlythis Home Assistant: should not set state when color is also set', async () => { + it('Home Assistant: should not set state when color is also set', async () => { settings.set(['homeassistant'], true); const device = zigbeeHerdsman.devices.bulb_color; controller.state.set(device.ieeeAddr, {state: 'ON'})