Add MQTT availability template and encoding (#60470)

* Add MQTT availability template and encoding

* use generic encoding field

* pylint and cleanup

* remove additional topic check
This commit is contained in:
Jan Bouwhuis 2021-12-02 10:21:31 +01:00 committed by GitHub
parent 42bae5439b
commit 3307e54363
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 119 additions and 2 deletions

View file

@ -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",

View file

@ -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
}

View file

@ -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",