From 3307e54363766bfef07f616ed05605632e78dacf Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 2 Dec 2021 10:21:31 +0100 Subject: [PATCH] Add MQTT availability template and encoding (#60470) * Add MQTT availability template and encoding * use generic encoding field * pylint and cleanup * remove additional topic check --- .../components/mqtt/abbreviations.py | 1 + homeassistant/components/mqtt/mixins.py | 26 ++++- tests/components/mqtt/test_discovery.py | 94 +++++++++++++++++++ 3 files changed, 119 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index c47b512b727..c5c70ad33a4 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -10,6 +10,7 @@ ABBREVIATIONS = { "avty": "availability", "avty_mode": "availability_mode", "avty_t": "availability_topic", + "avty_tpl": "availability_template", "away_mode_cmd_t": "away_mode_command_topic", "away_mode_stat_tpl": "away_mode_state_template", "away_mode_stat_t": "away_mode_state_topic", diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 3a374070f2f..713f0e5c030 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -21,6 +21,7 @@ from homeassistant.const import ( CONF_ICON, CONF_NAME, CONF_UNIQUE_ID, + CONF_VALUE_TEMPLATE, ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv @@ -42,6 +43,7 @@ from .const import ( ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC, CONF_AVAILABILITY, + CONF_ENCODING, CONF_QOS, CONF_TOPIC, DEFAULT_PAYLOAD_AVAILABLE, @@ -71,6 +73,7 @@ AVAILABILITY_LATEST = "latest" AVAILABILITY_MODES = [AVAILABILITY_ALL, AVAILABILITY_ANY, AVAILABILITY_LATEST] CONF_AVAILABILITY_MODE = "availability_mode" +CONF_AVAILABILITY_TEMPLATE = "availability_template" CONF_AVAILABILITY_TOPIC = "availability_topic" CONF_ENABLED_BY_DEFAULT = "enabled_by_default" CONF_PAYLOAD_AVAILABLE = "payload_available" @@ -112,6 +115,7 @@ MQTT_ATTRIBUTES_BLOCKED = { MQTT_AVAILABILITY_SINGLE_SCHEMA = vol.Schema( { vol.Exclusive(CONF_AVAILABILITY_TOPIC, "availability"): valid_subscribe_topic, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional( CONF_PAYLOAD_AVAILABLE, default=DEFAULT_PAYLOAD_AVAILABLE ): cv.string, @@ -138,6 +142,7 @@ MQTT_AVAILABILITY_LIST_SCHEMA = vol.Schema( CONF_PAYLOAD_NOT_AVAILABLE, default=DEFAULT_PAYLOAD_NOT_AVAILABLE, ): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ], ), @@ -335,6 +340,7 @@ class MqttAvailability(Entity): self._avail_topics[config[CONF_AVAILABILITY_TOPIC]] = { CONF_PAYLOAD_AVAILABLE: config[CONF_PAYLOAD_AVAILABLE], CONF_PAYLOAD_NOT_AVAILABLE: config[CONF_PAYLOAD_NOT_AVAILABLE], + CONF_AVAILABILITY_TEMPLATE: config.get(CONF_AVAILABILITY_TEMPLATE), } if CONF_AVAILABILITY in config: @@ -342,8 +348,22 @@ class MqttAvailability(Entity): self._avail_topics[avail[CONF_TOPIC]] = { CONF_PAYLOAD_AVAILABLE: avail[CONF_PAYLOAD_AVAILABLE], CONF_PAYLOAD_NOT_AVAILABLE: avail[CONF_PAYLOAD_NOT_AVAILABLE], + CONF_AVAILABILITY_TEMPLATE: avail.get(CONF_VALUE_TEMPLATE), } + for ( + topic, # pylint: disable=unused-variable + avail_topic_conf, + ) in self._avail_topics.items(): + tpl = avail_topic_conf[CONF_AVAILABILITY_TEMPLATE] + if tpl is None: + avail_topic_conf[CONF_AVAILABILITY_TEMPLATE] = lambda value: value + else: + tpl.hass = self.hass + avail_topic_conf[ + CONF_AVAILABILITY_TEMPLATE + ] = tpl.async_render_with_possible_json_value + self._avail_config = config async def _availability_subscribe_topics(self): @@ -354,10 +374,11 @@ class MqttAvailability(Entity): def availability_message_received(msg: ReceiveMessage) -> None: """Handle a new received MQTT availability message.""" topic = msg.topic - if msg.payload == self._avail_topics[topic][CONF_PAYLOAD_AVAILABLE]: + payload = self._avail_topics[topic][CONF_AVAILABILITY_TEMPLATE](msg.payload) + if payload == self._avail_topics[topic][CONF_PAYLOAD_AVAILABLE]: self._available[topic] = True self._available_latest = True - elif msg.payload == self._avail_topics[topic][CONF_PAYLOAD_NOT_AVAILABLE]: + elif payload == self._avail_topics[topic][CONF_PAYLOAD_NOT_AVAILABLE]: self._available[topic] = False self._available_latest = False @@ -372,6 +393,7 @@ class MqttAvailability(Entity): "topic": topic, "msg_callback": availability_message_received, "qos": self._avail_config[CONF_QOS], + "encoding": self._avail_config[CONF_ENCODING] or None, } for topic in self._avail_topics } diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index faa9f714ae0..6293d99aff4 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -737,6 +737,100 @@ async def test_discovery_expansion_3(hass, mqtt_mock, caplog): ) +async def test_discovery_expansion_without_encoding_and_value_template_1( + hass, mqtt_mock, caplog +): + """Test expansion of raw availability payload with a template as list.""" + data = ( + '{ "~": "some/base/topic",' + ' "name": "DiscoveryExpansionTest1",' + ' "stat_t": "test_topic/~",' + ' "cmd_t": "~/test_topic",' + ' "encoding":"",' + ' "availability": [{' + ' "topic":"~/avail_item1",' + ' "payload_available": "1",' + ' "payload_not_available": "0",' + ' "value_template":"{{ value | bitwise_and(1) }}"' + " }]," + ' "dev":{' + ' "ids":["5706DF"],' + ' "name":"DiscoveryExpansionTest1 Device",' + ' "mdl":"Generic",' + ' "sw":"1.2.3.4",' + ' "mf":"None",' + ' "sa":"default_area"' + " }" + "}" + ) + + async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data) + await hass.async_block_till_done() + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x01") + await hass.async_block_till_done() + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state is not None + assert state.name == "DiscoveryExpansionTest1" + assert ("switch", "bla") in hass.data[ALREADY_DISCOVERED] + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x00") + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state.state == STATE_UNAVAILABLE + + +async def test_discovery_expansion_without_encoding_and_value_template_2( + hass, mqtt_mock, caplog +): + """Test expansion of raw availability payload with a template directly.""" + data = ( + '{ "~": "some/base/topic",' + ' "name": "DiscoveryExpansionTest1",' + ' "stat_t": "test_topic/~",' + ' "cmd_t": "~/test_topic",' + ' "availability_topic":"~/avail_item1",' + ' "payload_available": "1",' + ' "payload_not_available": "0",' + ' "encoding":"",' + ' "availability_template":"{{ value | bitwise_and(1) }}",' + ' "dev":{' + ' "ids":["5706DF"],' + ' "name":"DiscoveryExpansionTest1 Device",' + ' "mdl":"Generic",' + ' "sw":"1.2.3.4",' + ' "mf":"None",' + ' "sa":"default_area"' + " }" + "}" + ) + + async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data) + await hass.async_block_till_done() + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x01") + await hass.async_block_till_done() + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state is not None + assert state.name == "DiscoveryExpansionTest1" + assert ("switch", "bla") in hass.data[ALREADY_DISCOVERED] + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "some/base/topic/avail_item1", b"\x00") + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state.state == STATE_UNAVAILABLE + + ABBREVIATIONS_WHITE_LIST = [ # MQTT client/server/trigger settings "CONF_BIRTH_MESSAGE",