diff --git a/lib/controller.js b/lib/controller.js index f0fe0b772..a4ea0b025 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -285,6 +285,8 @@ class Controller { attributes.forEach((a) => messagePayload.device[a] = device[a]); } + this.eventBus.emit('publishEntityState', {payload: messagePayload, entity}); + if (Object.entries(messagePayload).length) { if (settings.get().experimental.output === 'attribute_and_json') { await this.mqtt.publish(entity.name, JSON.stringify(messagePayload), options); diff --git a/lib/eventBus.js b/lib/eventBus.js index 274bd23a2..1dee91a27 100644 --- a/lib/eventBus.js +++ b/lib/eventBus.js @@ -3,6 +3,7 @@ const assert = require('assert'); const allowedEvents = [ 'deviceRemoved', // Device has been removed + 'publishEntityState', // Entity state will be published ]; class EventBus extends events.EventEmitter { diff --git a/lib/extension/homeassistant.js b/lib/extension/homeassistant.js index 7f1ec83c7..5cd9daef0 100644 --- a/lib/extension/homeassistant.js +++ b/lib/extension/homeassistant.js @@ -507,6 +507,22 @@ const cfg = { speeds: ['off', 'low', 'medium', 'high', 'on', 'auto', 'smart'], }, }, + + // Trigger + 'trigger_action': { + type: 'device_automation', + discovery_payload: { + automation_type: 'trigger', + type: 'action', + }, + }, + 'trigger_click': { + type: 'device_automation', + discovery_payload: { + automation_type: 'trigger', + type: 'click', + }, + }, }; const switchWithPostfix = (postfix) => { @@ -1274,6 +1290,7 @@ class HomeAssistant extends BaseExtension { // A map of all discoverd devices this.discovered = {}; + this.discoveredTriggers = {}; if (!settings.get().advanced.cache_state) { logger.warn('In order for HomeAssistant integration to work properly set `cache_state: true'); @@ -1287,6 +1304,7 @@ class HomeAssistant extends BaseExtension { this.statusTopic = settings.get().advanced.homeassistant_status_topic; this.eventBus.on('deviceRemoved', (data) => this.onDeviceRemoved(data.device)); + this.eventBus.on('publishEntityState', (data) => this.onPublishEntityState(data)); } onDeviceRemoved(device) { @@ -1300,6 +1318,39 @@ class HomeAssistant extends BaseExtension { } } + async onPublishEntityState(data) { + const key = ['action', 'click'].find((k) => data.payload.hasOwnProperty(k) && data.payload[k] !== ''); + + if (key && data.entity.type === 'device') { + const device = data.entity.device; + if (!this.discoveredTriggers[device.ieeeAddr]) { + this.discoveredTriggers[device.ieeeAddr] = new Set(); + } + + const value = data.payload[key]; + const discoveredKey = `${key}_${value}`; + + if (!this.discoveredTriggers[device.ieeeAddr].has(discoveredKey)) { + const config = cfg[`trigger_${key}`]; + config.object_id = `${key}_${value}`; + const topic = this.getDiscoveryTopic(config, device); + const mappedModel = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID); + const payload = { + ...config.discovery_payload, + subtype: value, + payload: value, + topic: `${settings.get().mqtt.base_topic}/${data.entity.name}/${key}`, + device: this.getDevicePayload(data.entity.settings, mappedModel), + }; + + this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0}, this.discoveryTopic); + this.discoveredTriggers[device.ieeeAddr].add(discoveredKey); + } + + await this.mqtt.publish(`${data.entity.name}/${key}`, value, {}); + } + } + async onMQTTConnected() { this.mqtt.subscribe(this.statusTopic); @@ -1364,13 +1415,7 @@ class HomeAssistant extends BaseExtension { payload.unique_id = `${entity.ID}_${config.object_id}_${settings.get().mqtt.base_topic}`; // Attributes for device registry - payload.device = { - identifiers: [`zigbee2mqtt_${entity.ID}`], - name: entity.friendlyName, - sw_version: `Zigbee2mqtt ${zigbee2mqttVersion}`, - model: `${mappedModel.description} (${mappedModel.model})`, - manufacturer: mappedModel.vendor, - }; + payload.device = this.getDevicePayload(entity, mappedModel); // Set availability payload // When using availability_timeout each device has it's own availability topic. @@ -1503,6 +1548,16 @@ class HomeAssistant extends BaseExtension { } } + getDevicePayload(entity, mappedModel) { + return { + identifiers: [`zigbee2mqtt_${entity.ID}`], + name: entity.friendlyName, + sw_version: `Zigbee2mqtt ${zigbee2mqttVersion}`, + model: `${mappedModel.description} (${mappedModel.model})`, + manufacturer: mappedModel.vendor, + }; + } + getDiscoveryTopic(config, device) { return `${config.type}/${device.ieeeAddr}/${config.object_id}/config`; } diff --git a/test/homeassistant.test.js b/test/homeassistant.test.js index e497e3e56..1669047cf 100644 --- a/test/homeassistant.test.js +++ b/test/homeassistant.test.js @@ -719,4 +719,65 @@ describe('HomeAssistant extension', () => { expect.any(Function), ); }); + + it('Should discover trigger when click is published', async () => { + controller = new Controller(false); + await controller.start(); + await flushPromises(); + MQTT.publish.mockClear(); + + const device = zigbeeHerdsman.devices.WXKG11LM; + const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + + const discoverPayload = { + "automation_type":"trigger", + "type":"click", + "subtype":"single", + "payload":"single", + "topic":"zigbee2mqtt/button/click", + "device":{ + "identifiers":[ + "zigbee2mqtt_0x0017880104e45520" + ], + "name":"button", + "sw_version": this.version, + "model":"Aqara wireless switch (WXKG11LM)", + "manufacturer":"Xiaomi" + } + }; + + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/device_automation/0x0017880104e45520/click_single/config', + JSON.stringify(discoverPayload), + { retain: true, qos: 0 }, + expect.any(Function), + ); + + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/button/click', + 'single', + { retain: false, qos: 0 }, + expect.any(Function), + ); + + // Should only discover it once + MQTT.publish.mockClear(); + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).not.toHaveBeenCalledWith( + 'homeassistant/device_automation/0x0017880104e45520/click_single/config', + JSON.stringify(discoverPayload), + { retain: true, qos: 0 }, + expect.any(Function), + ); + + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/button/click', + 'single', + { retain: false, qos: 0 }, + expect.any(Function), + ); + }); });