diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index 9a95774b2b..8ea62786d8 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -385,6 +385,7 @@ export default class HomeAssistant extends Extension { private discoveryRegex: RegExp; private discoveryRegexWoTopic = new RegExp(`(.*)/(.*)/(.*)/config`); private statusTopic: string; + private legacyActionSensor: boolean; private experimentalEventEntities: boolean; // @ts-expect-error initialized in `start` private zigbee2MQTTVersion: string; @@ -416,6 +417,7 @@ export default class HomeAssistant extends Extension { this.discoveryTopic = haSettings.discovery_topic; this.discoveryRegex = new RegExp(`${haSettings.discovery_topic}/(.*)/(.*)/(.*)/config`); this.statusTopic = haSettings.status_topic; + this.legacyActionSensor = haSettings.legacy_action_sensor; this.experimentalEventEntities = haSettings.experimental_event_entities; if (haSettings.discovery_topic === settings.get().mqtt.base_topic) { throw new Error(`'homeassistant.discovery_topic' cannot not be equal to the 'mqtt.base_topic' (got '${settings.get().mqtt.base_topic}')`); @@ -1079,8 +1081,9 @@ export default class HomeAssistant extends Extension { }, }); } - // Don't expose action sensor, use MQTT device trigger instead - break; + if (!this.legacyActionSensor) { + break; + } } const valueTemplate = firstExpose.access & ACCESS_STATE ? `{{ value_json.${firstExpose.property} }}` : undefined; @@ -1274,13 +1277,22 @@ export default class HomeAssistant extends Extension { } } + /** + * Publish an empty value for click and action payload, in this way Home Assistant + * can use Home Assistant entities in automations. + * https://github.com/Koenkk/zigbee2mqtt/issues/959#issuecomment-480341347 + */ + if (this.legacyActionSensor && data.message.action) { + await this.publishEntityState(data.entity, {action: ''}); + } + /** * Implements the MQTT device trigger (https://www.home-assistant.io/integrations/device_trigger.mqtt/) * The MQTT device trigger does not support JSON parsing, so it cannot listen to zigbee2mqtt/my_device * Whenever a device publish an {action: *} we discover an MQTT device trigger sensor * and republish it to zigbee2mqtt/my_device/action */ - if (entity.isDevice() && entity.definition && 'action' in data.message) { + if (entity.isDevice() && entity.definition && data.message.action) { const value = data.message['action'].toString(); await this.publishDeviceTriggerDiscover(entity, 'action', value); await this.mqtt.publish(`${data.entity.name}/action`, value, {}); diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index 9db2ea0bda..09e0b85af8 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -100,6 +100,7 @@ declare global { discovery_topic: string; status_topic: string; experimental_event_entities: boolean; + legacy_action_sensor: boolean; }; availability?: { active: {timeout: number}; diff --git a/lib/util/settings.schema.json b/lib/util/settings.schema.json index 6618dfa4ee..4d7916e906 100644 --- a/lib/util/settings.schema.json +++ b/lib/util/settings.schema.json @@ -29,6 +29,12 @@ "requiresRestart": true, "examples": ["homeassistant/status"] }, + "legacy_action_sensor": { + "type": "boolean", + "title": "Home Assistant legacy action sensors", + "description": "Home Assistant legacy actions sensor, when enabled a action sensor will be discoverd and an empty `action` will be send after every published action.", + "default": false + }, "experimental_event_entities": { "type": "boolean", "title": "Home Assistant experimental event entities", diff --git a/lib/util/settings.ts b/lib/util/settings.ts index aaf4b09e93..a471dcd734 100644 --- a/lib/util/settings.ts +++ b/lib/util/settings.ts @@ -113,6 +113,7 @@ function loadSettingsWithDefaults(): void { const defaults = { discovery_topic: 'homeassistant', status_topic: 'hass/status', + legacy_action_sensor: false, experimental_event_entities: false, }; const s = typeof _settingsWithDefaults.homeassistant === 'object' ? _settingsWithDefaults.homeassistant : {}; diff --git a/test/extensions/homeassistant.test.ts b/test/extensions/homeassistant.test.ts index cd831fbfbc..e7c361ea1d 100644 --- a/test/extensions/homeassistant.test.ts +++ b/test/extensions/homeassistant.test.ts @@ -417,6 +417,12 @@ describe('Extension: HomeAssistant', () => { retain: true, qos: 1, }); + + // Should NOT discovery leagcy action sensor as option is not enabled. + expect(mockMQTT.publishAsync).not.toHaveBeenCalledWith('homeassistant/sensor/0x0017880104e45520/action/config', expect.any(String), { + retain: true, + qos: 1, + }); }); it.each([ @@ -2458,4 +2464,44 @@ describe('Extension: HomeAssistant', () => { qos: 1, }); }); + + it('Legacy action sensor', async () => { + settings.set(['homeassistant'], {legacy_action_sensor: true}); + await resetExtension(); + + // Should discovery action sensor + expect(mockMQTT.publishAsync).toHaveBeenCalledWith('homeassistant/sensor/0x0017880104e45520/action/config', expect.any(String), { + retain: true, + qos: 1, + }); + + // Should counter an action payload with an empty payload + mockMQTT.publishAsync.mockClear(); + const device = devices.WXKG11LM; + const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await mockZHEvents.message(payload); + await flushPromises(); + expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button'); + expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({ + action: 'single', + battery: null, + linkquality: null, + voltage: null, + power_outage_count: null, + device_temperature: null, + }); + expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/button'); + expect(JSON.parse(mockMQTT.publishAsync.mock.calls[1][1])).toStrictEqual({ + action: '', + battery: null, + linkquality: null, + voltage: null, + power_outage_count: null, + device_temperature: null, + }); + expect(mockMQTT.publishAsync.mock.calls[1][2]).toStrictEqual({qos: 0, retain: false}); + expect(mockMQTT.publishAsync.mock.calls[2][0]).toStrictEqual('homeassistant/device_automation/0x0017880104e45520/action_single/config'); + expect(mockMQTT.publishAsync.mock.calls[3][0]).toStrictEqual('zigbee2mqtt/button/action'); + }); });