From a600f5a99933884a5b169ba7a5f774c590ff8e52 Mon Sep 17 00:00:00 2001 From: Laurent van den Bos Date: Sat, 3 Aug 2024 11:52:15 +0200 Subject: [PATCH 1/9] Fire event when group gets removed --- lib/eventBus.ts | 7 +++++++ lib/extension/bridge.ts | 2 ++ lib/extension/homeassistant.ts | 12 ++++++++++++ lib/types/types.d.ts | 1 + 4 files changed, 22 insertions(+) diff --git a/lib/eventBus.ts b/lib/eventBus.ts index aef2c95b62..4eb763e59e 100644 --- a/lib/eventBus.ts +++ b/lib/eventBus.ts @@ -48,6 +48,13 @@ export default class EventBus { this.on('deviceRemoved', callback, key); } + public emitGroupRemoved(data: eventdata.GroupRemoved) { + this.emitter.emit('groupRemoved', data); + } + public onGroupRemoved(key: ListenerKey, callback: (data: eventdata.GroupRemoved) => void): void { + this.on('groupRemoved', callback, key); + } + public emitLastSeenChanged(data: eventdata.LastSeenChanged): void { this.emitter.emit('lastSeenChanged', data); } diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index caee3da2a1..26e0627408 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -640,6 +640,8 @@ export default class Bridge extends Extension { // Fire event if (entity instanceof Device) { this.eventBus.emitDeviceRemoved({ieeeAddr, name}); + } else { + this.eventBus.emitGroupRemoved({groupID: entityID as number, name: name}) } // Remove from configuration.yaml diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index be0d3a8155..08366d3e3c 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -202,6 +202,7 @@ export default class HomeAssistant extends Extension { this.bridge = this.getBridgeEntity(await this.zigbee.getCoordinatorVersion()); this.bridgeIdentifier = this.getDevicePayload(this.bridge).identifiers[0]; this.eventBus.onDeviceRemoved(this, this.onDeviceRemoved); + this.eventBus.onGroupRemoved(this, this.onGroupRemoved); this.eventBus.onMQTTMessage(this, this.onMQTTMessage); this.eventBus.onEntityRenamed(this, this.onEntityRenamed); this.eventBus.onPublishEntityState(this, this.onPublishEntityState); @@ -1213,6 +1214,17 @@ export default class HomeAssistant extends Extension { delete this.discovered[data.ieeeAddr]; } + @bind async onGroupRemoved(data: eventdata.GroupRemoved): Promise { + logger.debug(`Clearing Home Assistant discovery for group '${data.name}'`); + const discovered = this.getDiscovered(data.groupID.toString()) + + for (const topic of Object.keys(discovered.messages)) { + await this.mqtt.publish(topic, null, {retain: true, qos: 1}, this.discoveryTopic, false, false); + } + + delete this.discovered[data.groupID.toString()] + } + @bind async onGroupMembersChanged(data: eventdata.GroupMembersChanged): Promise { await this.discover(data.group); } diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index ff23add570..9cc2b84029 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -74,6 +74,7 @@ declare global { namespace eventdata { type EntityRenamed = {entity: Device | Group; homeAssisantRename: boolean; from: string; to: string}; type DeviceRemoved = {ieeeAddr: string; name: string}; + type GroupRemoved = {groupID: number, name: string}; type MQTTMessage = {topic: string; message: string}; type MQTTMessagePublished = {topic: string; payload: string; options: {retain: boolean; qos: number}}; type StateChange = { From 8a9cd1d7ee3efcf689b56ec5574f94af3cade2ef Mon Sep 17 00:00:00 2001 From: Laurent van den Bos Date: Sat, 3 Aug 2024 12:03:36 +0200 Subject: [PATCH 2/9] Add a unit test for the new behavior --- test/homeassistant.test.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/homeassistant.test.js b/test/homeassistant.test.js index 63f903ccd9..c68f406bbe 100644 --- a/test/homeassistant.test.js +++ b/test/homeassistant.test.js @@ -1401,6 +1401,22 @@ describe('HomeAssistant extension', () => { ); }); + it('Should clear discovery when group is removed', async () => { + MQTT.publish.mockClear(); + MQTT.events.message( + 'zigbee2mqtt/bridge/request/group/remove', + stringify({id: 'ha_discovery_group'}), + ); + await flushPromises(); + + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/light/1221051039810110150109113116116_9/light/config', + null, + {retain: true, qos: 1}, + expect.any(Function), + ); + }); + it('Should refresh discovery when device is renamed', async () => { await MQTT.events.message( 'homeassistant/device_automation/0x0017880104e45522/action_double/config', From 8deb8092560bb236f383582aaf39b6e6401ba24a Mon Sep 17 00:00:00 2001 From: Laurent van den Bos Date: Sat, 3 Aug 2024 12:03:57 +0200 Subject: [PATCH 3/9] Make prettier happy --- lib/extension/bridge.ts | 2 +- lib/extension/homeassistant.ts | 4 ++-- lib/types/types.d.ts | 2 +- test/homeassistant.test.js | 5 +---- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index 26e0627408..21e8401bcf 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -641,7 +641,7 @@ export default class Bridge extends Extension { if (entity instanceof Device) { this.eventBus.emitDeviceRemoved({ieeeAddr, name}); } else { - this.eventBus.emitGroupRemoved({groupID: entityID as number, name: name}) + this.eventBus.emitGroupRemoved({groupID: entityID as number, name: name}); } // Remove from configuration.yaml diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index 08366d3e3c..719c010b44 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -1216,13 +1216,13 @@ export default class HomeAssistant extends Extension { @bind async onGroupRemoved(data: eventdata.GroupRemoved): Promise { logger.debug(`Clearing Home Assistant discovery for group '${data.name}'`); - const discovered = this.getDiscovered(data.groupID.toString()) + const discovered = this.getDiscovered(data.groupID.toString()); for (const topic of Object.keys(discovered.messages)) { await this.mqtt.publish(topic, null, {retain: true, qos: 1}, this.discoveryTopic, false, false); } - delete this.discovered[data.groupID.toString()] + delete this.discovered[data.groupID.toString()]; } @bind async onGroupMembersChanged(data: eventdata.GroupMembersChanged): Promise { diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index 9cc2b84029..c150d59302 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -74,7 +74,7 @@ declare global { namespace eventdata { type EntityRenamed = {entity: Device | Group; homeAssisantRename: boolean; from: string; to: string}; type DeviceRemoved = {ieeeAddr: string; name: string}; - type GroupRemoved = {groupID: number, name: string}; + type GroupRemoved = {groupID: number; name: string}; type MQTTMessage = {topic: string; message: string}; type MQTTMessagePublished = {topic: string; payload: string; options: {retain: boolean; qos: number}}; type StateChange = { diff --git a/test/homeassistant.test.js b/test/homeassistant.test.js index c68f406bbe..ec7aa4fd7d 100644 --- a/test/homeassistant.test.js +++ b/test/homeassistant.test.js @@ -1403,10 +1403,7 @@ describe('HomeAssistant extension', () => { it('Should clear discovery when group is removed', async () => { MQTT.publish.mockClear(); - MQTT.events.message( - 'zigbee2mqtt/bridge/request/group/remove', - stringify({id: 'ha_discovery_group'}), - ); + MQTT.events.message('zigbee2mqtt/bridge/request/group/remove', stringify({id: 'ha_discovery_group'})); await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( From 56aa0b0c386787559fc8357701595702aad57669 Mon Sep 17 00:00:00 2001 From: Laurent van den Bos Date: Sat, 3 Aug 2024 12:11:48 +0200 Subject: [PATCH 4/9] Add missing return type --- lib/eventBus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/eventBus.ts b/lib/eventBus.ts index 4eb763e59e..a9d6342657 100644 --- a/lib/eventBus.ts +++ b/lib/eventBus.ts @@ -48,7 +48,7 @@ export default class EventBus { this.on('deviceRemoved', callback, key); } - public emitGroupRemoved(data: eventdata.GroupRemoved) { + public emitGroupRemoved(data: eventdata.GroupRemoved): void { this.emitter.emit('groupRemoved', data); } public onGroupRemoved(key: ListenerKey, callback: (data: eventdata.GroupRemoved) => void): void { From 8bc11c01d75b33d3b5a42c41d11bcd89d94e8a26 Mon Sep 17 00:00:00 2001 From: Laurent van den Bos Date: Sun, 4 Aug 2024 21:23:54 +0200 Subject: [PATCH 5/9] Merge device and group removal events --- lib/eventBus.ts | 11 ++--------- lib/extension/availability.ts | 2 +- lib/extension/bridge.ts | 4 ++-- lib/extension/homeassistant.ts | 20 ++++---------------- lib/extension/legacy/bridgeLegacy.ts | 2 +- lib/types/types.d.ts | 3 +-- 6 files changed, 11 insertions(+), 31 deletions(-) diff --git a/lib/eventBus.ts b/lib/eventBus.ts index a9d6342657..5a360d2179 100644 --- a/lib/eventBus.ts +++ b/lib/eventBus.ts @@ -41,20 +41,13 @@ export default class EventBus { this.on('deviceRenamed', callback, key); } - public emitDeviceRemoved(data: eventdata.DeviceRemoved): void { + public emitEntityRemoved(data: eventdata.EntityRemoved): void { this.emitter.emit('deviceRemoved', data); } - public onDeviceRemoved(key: ListenerKey, callback: (data: eventdata.DeviceRemoved) => void): void { + public onEntityRemoved(key: ListenerKey, callback: (data: eventdata.EntityRemoved) => void): void { this.on('deviceRemoved', callback, key); } - public emitGroupRemoved(data: eventdata.GroupRemoved): void { - this.emitter.emit('groupRemoved', data); - } - public onGroupRemoved(key: ListenerKey, callback: (data: eventdata.GroupRemoved) => void): void { - this.on('groupRemoved', callback, key); - } - public emitLastSeenChanged(data: eventdata.LastSeenChanged): void { this.emitter.emit('lastSeenChanged', data); } diff --git a/lib/extension/availability.ts b/lib/extension/availability.ts index 0789a38f46..b2d6098302 100644 --- a/lib/extension/availability.ts +++ b/lib/extension/availability.ts @@ -128,7 +128,7 @@ export default class Availability extends Extension { } }); - this.eventBus.onDeviceRemoved(this, (data) => clearTimeout(this.timers[data.ieeeAddr])); + this.eventBus.onEntityRemoved(this, (data) => data.type == 'device' && clearTimeout(this.timers[data.id])); this.eventBus.onDeviceLeave(this, (data) => clearTimeout(this.timers[data.ieeeAddr])); this.eventBus.onDeviceAnnounce(this, (data) => this.retrieveState(data.device)); this.eventBus.onLastSeenChanged(this, this.onLastSeenChanged); diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index f03bbc26b9..41f0efb739 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -641,9 +641,9 @@ export default class Bridge extends Extension { // Fire event if (entity instanceof Device) { - this.eventBus.emitDeviceRemoved({ieeeAddr, name}); + this.eventBus.emitEntityRemoved({id: ieeeAddr, name: name, type: 'device'}); } else { - this.eventBus.emitGroupRemoved({groupID: entityID as number, name: name}); + this.eventBus.emitEntityRemoved({id: entityID as number, name: name, type: 'group'}); } // Remove from configuration.yaml diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index 4211e289d4..566c9c13ba 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -201,8 +201,7 @@ export default class HomeAssistant extends Extension { this.discoveryOrigin = {name: 'Zigbee2MQTT', sw: this.zigbee2MQTTVersion, url: 'https://www.zigbee2mqtt.io'}; this.bridge = this.getBridgeEntity(await this.zigbee.getCoordinatorVersion()); this.bridgeIdentifier = this.getDevicePayload(this.bridge).identifiers[0]; - this.eventBus.onDeviceRemoved(this, this.onDeviceRemoved); - this.eventBus.onGroupRemoved(this, this.onGroupRemoved); + this.eventBus.onEntityRemoved(this, this.onEntityRemoved); this.eventBus.onMQTTMessage(this, this.onMQTTMessage); this.eventBus.onEntityRenamed(this, this.onEntityRenamed); this.eventBus.onPublishEntityState(this, this.onPublishEntityState); @@ -1195,26 +1194,15 @@ export default class HomeAssistant extends Extension { return discoveryEntries; } - @bind async onDeviceRemoved(data: eventdata.DeviceRemoved): Promise { + @bind async onEntityRemoved(data: eventdata.EntityRemoved): Promise { logger.debug(`Clearing Home Assistant discovery for '${data.name}'`); - const discovered = this.getDiscovered(data.ieeeAddr); + const discovered = this.getDiscovered(data.id.toString()); for (const topic of Object.keys(discovered.messages)) { await this.mqtt.publish(topic, null, {retain: true, qos: 1}, this.discoveryTopic, false, false); } - delete this.discovered[data.ieeeAddr]; - } - - @bind async onGroupRemoved(data: eventdata.GroupRemoved): Promise { - logger.debug(`Clearing Home Assistant discovery for group '${data.name}'`); - const discovered = this.getDiscovered(data.groupID.toString()); - - for (const topic of Object.keys(discovered.messages)) { - await this.mqtt.publish(topic, null, {retain: true, qos: 1}, this.discoveryTopic, false, false); - } - - delete this.discovered[data.groupID.toString()]; + delete this.discovered[data.id.toString()]; } @bind async onGroupMembersChanged(data: eventdata.GroupMembersChanged): Promise { diff --git a/lib/extension/legacy/bridgeLegacy.ts b/lib/extension/legacy/bridgeLegacy.ts index e8240b4cf2..04ea0bdaaa 100644 --- a/lib/extension/legacy/bridgeLegacy.ts +++ b/lib/extension/legacy/bridgeLegacy.ts @@ -295,7 +295,7 @@ export default class BridgeLegacy extends Extension { const cleanup = async (): Promise => { // Fire event - this.eventBus.emitDeviceRemoved({ieeeAddr, name}); + this.eventBus.emitEntityRemoved({id: ieeeAddr, name}); // Remove from configuration.yaml settings.removeDevice(entity.ieeeAddr); diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index c150d59302..49fc62600d 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -73,8 +73,7 @@ declare global { namespace eventdata { type EntityRenamed = {entity: Device | Group; homeAssisantRename: boolean; from: string; to: string}; - type DeviceRemoved = {ieeeAddr: string; name: string}; - type GroupRemoved = {groupID: number; name: string}; + type EntityRemoved = {id: number | string; name: string; type: 'device' | 'group'}; type MQTTMessage = {topic: string; message: string}; type MQTTMessagePublished = {topic: string; payload: string; options: {retain: boolean; qos: number}}; type StateChange = { From 848999458e7bc0027b08e7a0946941273cfe4674 Mon Sep 17 00:00:00 2001 From: Laurent van den Bos Date: Sun, 4 Aug 2024 21:26:18 +0200 Subject: [PATCH 6/9] Unbreak bridgeLegacy.ts --- lib/extension/legacy/bridgeLegacy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/extension/legacy/bridgeLegacy.ts b/lib/extension/legacy/bridgeLegacy.ts index 04ea0bdaaa..93ddcd00ad 100644 --- a/lib/extension/legacy/bridgeLegacy.ts +++ b/lib/extension/legacy/bridgeLegacy.ts @@ -295,7 +295,7 @@ export default class BridgeLegacy extends Extension { const cleanup = async (): Promise => { // Fire event - this.eventBus.emitEntityRemoved({id: ieeeAddr, name}); + this.eventBus.emitEntityRemoved({id: ieeeAddr, name: name, type: 'device'}); // Remove from configuration.yaml settings.removeDevice(entity.ieeeAddr); From fd4e3322264c18df1fcb868126892d7196983daa Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Sun, 4 Aug 2024 22:07:31 +0200 Subject: [PATCH 7/9] Update homeassistant.ts --- lib/extension/homeassistant.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index 566c9c13ba..f5ec26aa85 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -244,8 +244,8 @@ export default class HomeAssistant extends Extension { this.eventBus.emitPublishAvailability(); } - private getDiscovered(entity: Device | Group | Bridge | string): Discovered { - const ID = typeof entity === 'string' ? entity : entity.ID; + private getDiscovered(entity: Device | Group | Bridge | string | number): Discovered { + const ID = typeof entity === 'string' || typeof entity === 'number' ? entity : entity.ID; if (!(ID in this.discovered)) { this.discovered[ID] = {messages: {}, triggers: new Set(), mockProperties: new Set(), discovered: false}; } @@ -1196,13 +1196,13 @@ export default class HomeAssistant extends Extension { @bind async onEntityRemoved(data: eventdata.EntityRemoved): Promise { logger.debug(`Clearing Home Assistant discovery for '${data.name}'`); - const discovered = this.getDiscovered(data.id.toString()); + const discovered = this.getDiscovered(data.id); for (const topic of Object.keys(discovered.messages)) { await this.mqtt.publish(topic, null, {retain: true, qos: 1}, this.discoveryTopic, false, false); } - delete this.discovered[data.id.toString()]; + delete this.discovered[data.id]; } @bind async onGroupMembersChanged(data: eventdata.GroupMembersChanged): Promise { From 420efb5cd760f1c646e3ca205c9141e817ebb241 Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Sun, 4 Aug 2024 22:10:21 +0200 Subject: [PATCH 8/9] Update bridge.ts --- lib/extension/bridge.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index 41f0efb739..d8f0a3b138 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -618,7 +618,6 @@ export default class Bridge extends Extension { try { logger.info(`Removing ${entityType} '${entity.name}'${blockForceLog}`); - const ieeeAddr = entity.isDevice() && entity.ieeeAddr; const name = entity.name; if (entity instanceof Device) { @@ -641,9 +640,9 @@ export default class Bridge extends Extension { // Fire event if (entity instanceof Device) { - this.eventBus.emitEntityRemoved({id: ieeeAddr, name: name, type: 'device'}); + this.eventBus.emitEntityRemoved({id: entityID, name: name, type: 'device'}); } else { - this.eventBus.emitEntityRemoved({id: entityID as number, name: name, type: 'group'}); + this.eventBus.emitEntityRemoved({id: entityID, name: name, type: 'group'}); } // Remove from configuration.yaml From 9cf8530f8a4770e067e3d3bf005e4168ffbe3dd8 Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Sun, 4 Aug 2024 22:10:43 +0200 Subject: [PATCH 9/9] Update bridge.ts --- lib/extension/bridge.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index d8f0a3b138..40266ac3de 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -640,9 +640,9 @@ export default class Bridge extends Extension { // Fire event if (entity instanceof Device) { - this.eventBus.emitEntityRemoved({id: entityID, name: name, type: 'device'}); + this.eventBus.emitEntityRemoved({id: entityID, name, type: 'device'}); } else { - this.eventBus.emitEntityRemoved({id: entityID, name: name, type: 'group'}); + this.eventBus.emitEntityRemoved({id: entityID, name, type: 'group'}); } // Remove from configuration.yaml