From 9edbe5a2a851a628bd63afde94dc4e02d67d4727 Mon Sep 17 00:00:00 2001 From: Yip Rui Fung Date: Sat, 10 Jul 2021 01:37:32 +0800 Subject: [PATCH] Discover enum exposes as select entities in Home Assistant (#7980) * Read/write exposed enums are now discovered as MQTT Select entities in home assistant 2021.7. * Read/Write enums are now exposed as a Select in addition to the existing sensor, for compatibility. Also, the sensor will be disabled by default for new entities, shouldn't effect existing installations. * Add support for returning multiple config from a single call to exposeToConfig. * Update homeassistant.js Co-authored-by: Koen Kanters --- lib/extension/homeassistant.js | 86 +++++++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 18 deletions(-) diff --git a/lib/extension/homeassistant.js b/lib/extension/homeassistant.js index fa9cc55a1..de5151062 100644 --- a/lib/extension/homeassistant.js +++ b/lib/extension/homeassistant.js @@ -72,7 +72,7 @@ class HomeAssistant extends Extension { assert(entityType === 'device' || groupSupportedTypes.includes(firstExpose.type), `Unsupported expose type ${firstExpose.type} for group`); - let discoveryEntry = null; + const discoveryEntries = []; const endpoint = entityType === 'device' ? exposes[0].endpoint : undefined; const getProperty = (feature) => entityType === 'group' ? featurePropertyWithoutEndpoint(feature) : feature.property; @@ -84,7 +84,7 @@ class HomeAssistant extends Extension { const hasBrightness = exposes.find((expose) => expose.features.find((e) => e.name === 'brightness')); const hasColorTemp = exposes.find((expose) => expose.features.find((e) => e.name === 'color_temp')); - discoveryEntry = { + const discoveryEntry = { type: 'light', object_id: endpoint ? `light_${endpoint}` : 'light', discovery_payload: { @@ -119,10 +119,12 @@ class HomeAssistant extends Extension { discoveryEntry.discovery_payload.effect = true; discoveryEntry.discovery_payload.effect_list = effect.values; } + + discoveryEntries.push(discoveryEntry); } else if (firstExpose.type === 'switch') { const state = firstExpose.features.find((f) => f.name === 'state'); const property = getProperty(state); - discoveryEntry = { + const discoveryEntry = { type: 'switch', object_id: endpoint ? `switch_${endpoint}` : 'switch', discovery_payload: { @@ -145,6 +147,8 @@ class HomeAssistant extends Extension { discoveryEntry.discovery_payload.icon = 'mdi:window-open-variant'; } } + + discoveryEntries.push(discoveryEntry); } else if (firstExpose.type === 'climate') { const setpointProperties = ['occupied_heating_setpoint', 'current_heating_setpoint']; const setpoint = firstExpose.features.find((f) => setpointProperties.includes(f.name)); @@ -152,7 +156,7 @@ class HomeAssistant extends Extension { const temperature = firstExpose.features.find((f) => f.name === 'local_temperature'); assert(temperature, 'No temperature found'); - discoveryEntry = { + const discoveryEntry = { type: 'climate', object_id: endpoint ? `climate_${endpoint}` : 'climate', discovery_payload: { @@ -237,11 +241,13 @@ class HomeAssistant extends Extension { if (firstExpose.endpoint) { discoveryEntry.discovery_payload.state_topic_postfix = firstExpose.endpoint; } + + discoveryEntries.push(discoveryEntry); } else if (firstExpose.type === 'lock') { assert(!endpoint, `Endpoint not supported for lock type`); const state = firstExpose.features.find((f) => f.name === 'state'); assert(state, 'No state found'); - discoveryEntry = { + const discoveryEntry = { type: 'lock', object_id: 'lock', discovery_payload: { @@ -272,12 +278,14 @@ class HomeAssistant extends Extension { if (state.property !== 'state') { discoveryEntry.discovery_payload.command_topic_postfix = state.property; } + + discoveryEntries.push(discoveryEntry); } else if (firstExpose.type === 'cover') { assert(!endpoint, `Endpoint not supported for cover type`); const hasPosition = exposes.find((expose) => expose.features.find((e) => e.name === 'position')); const hasTilt = exposes.find((expose) => expose.features.find((e) => e.name === 'tilt')); - discoveryEntry = { + const discoveryEntry = { type: 'cover', object_id: 'cover', discovery_payload: { @@ -306,9 +314,11 @@ class HomeAssistant extends Extension { tilt_status_template: '{{ value_json.tilt }}', }; } + + discoveryEntries.push(discoveryEntry); } else if (firstExpose.type === 'fan') { assert(!endpoint, `Endpoint not supported for fan type`); - discoveryEntry = { + const discoveryEntry = { type: 'fan', object_id: 'fan', discovery_payload: { @@ -367,6 +377,8 @@ class HomeAssistant extends Extension { ` else 'None' | default('None') }}`; discoveryEntry.discovery_payload.preset_modes = presets; } + + discoveryEntries.push(discoveryEntry); } else if (firstExpose.type === 'binary') { const lookup = { occupancy: {device_class: 'motion'}, @@ -388,7 +400,7 @@ class HomeAssistant extends Extension { * Dont expose boolean values for now: https://github.com/Koenkk/zigbee2mqtt/issues/7797 */ if (firstExpose.access & ACCESS_SET) { - discoveryEntry = { + const discoveryEntry = { type: 'switch', object_id: endpoint ? `switch_${firstExpose.name}_${endpoint}` : @@ -405,8 +417,9 @@ class HomeAssistant extends Extension { ...(lookup[firstExpose.name] || {}), }, }; + discoveryEntries.push(discoveryEntry); } else { - discoveryEntry = { + const discoveryEntry = { type: 'binary_sensor', object_id: endpoint ? `${firstExpose.name}_${endpoint}` : `${firstExpose.name}`, discovery_payload: { @@ -416,6 +429,7 @@ class HomeAssistant extends Extension { ...(lookup[firstExpose.name] || {}), }, }; + discoveryEntries.push(discoveryEntry); } } else if (firstExpose.type === 'numeric') { const lookup = { @@ -459,7 +473,7 @@ class HomeAssistant extends Extension { z_axis: {icon: 'mdi:axis-z-arrow'}, }; - discoveryEntry = { + const discoveryEntry = { type: 'sensor', object_id: endpoint ? `${firstExpose.name}_${endpoint}` : `${firstExpose.name}`, discovery_payload: { @@ -468,13 +482,50 @@ class HomeAssistant extends Extension { ...lookup[firstExpose.name], }, }; - } else if (firstExpose.type === 'enum' || firstExpose.type === 'text' || firstExpose.type === 'composite') { + discoveryEntries.push(discoveryEntry); + } else if (firstExpose.type === 'enum') { + const lookup = { + action: {icon: 'mdi:gesture-double-tap'}, + }; + + if (firstExpose.access & ACCESS_STATE) { + discoveryEntries.push({ + type: 'sensor', + object_id: firstExpose.property, + discovery_payload: { + value_template: `{{ value_json.${firstExpose.property} }}`, + ...lookup[firstExpose.name], + }, + }); + + /** + * If enum attribute has SET access then expose as SELECT entity too. + */ + if ((firstExpose.access & ACCESS_SET)) { + // Make the sensor disabled by default for new entities. + discoveryEntries[discoveryEntries.length - 1].discovery_payload.enabled_by_default = false; + discoveryEntries.push({ + type: 'select', + object_id: firstExpose.property, + discovery_payload: { + value_template: `{{ value_json.${firstExpose.property} }}`, + state_topic: true, + command_topic_prefix: endpoint, + command_topic: true, + command_topic_postfix: firstExpose.property, + options: firstExpose.values, + ...lookup[firstExpose.name], + }, + }); + } + } + } else if (firstExpose.type === 'text' || firstExpose.type === 'composite') { if (firstExpose.access & ACCESS_STATE) { const lookup = { action: {icon: 'mdi:gesture-double-tap'}, }; - discoveryEntry = { + const discoveryEntry = { type: 'sensor', object_id: firstExpose.property, discovery_payload: { @@ -482,12 +533,13 @@ class HomeAssistant extends Extension { ...lookup[firstExpose.name], }, }; + discoveryEntries.push(discoveryEntry); } } else { throw new Error(`Unsupported exposes type: '${firstExpose.type}'`); } - return discoveryEntry; + return discoveryEntries; } populateMapping() { @@ -517,10 +569,7 @@ class HomeAssistant extends Extension { } for (const expose of def.exposes) { - const discoveryEntry = this.exposeToConfig([expose], 'device', def); - if (discoveryEntry) { - this.mapping[def.model].push(discoveryEntry); - } + this.mapping[def.model].push(...this.exposeToConfig([expose], 'device', def)); } } @@ -666,7 +715,8 @@ class HomeAssistant extends Extension { } }); - configs = Object.values(exposesByType).map((exposes) => this.exposeToConfig(exposes, 'group')); + configs = [].concat(...Object.values(exposesByType) + .map((exposes) => this.exposeToConfig(exposes, 'group'))); } if (isDevice && resolvedEntity.definition.hasOwnProperty('ota')) {