Improve MQTT update platform ()

* Allow JSON as state_topic payload

* Add title

* Add release_url

* Add release_summary

* Add entity_picture

* Fix typo

* Add abbreviations
This commit is contained in:
Maciej Bieniek 2022-10-28 17:05:43 +02:00 committed by Paulus Schoutsen
parent 9b87f7f6f9
commit 4684101a85
3 changed files with 211 additions and 9 deletions
homeassistant/components/mqtt
tests/components/mqtt

View file

@ -52,6 +52,7 @@ ABBREVIATIONS = {
"e": "encoding",
"en": "enabled_by_default",
"ent_cat": "entity_category",
"ent_pic": "entity_picture",
"err_t": "error_topic",
"err_tpl": "error_template",
"fanspd_t": "fan_speed_topic",
@ -169,6 +170,8 @@ ABBREVIATIONS = {
"pr_mode_val_tpl": "preset_mode_value_template",
"pr_modes": "preset_modes",
"r_tpl": "red_template",
"rel_s": "release_summary",
"rel_u": "release_url",
"ret": "retain",
"rgb_cmd_tpl": "rgb_command_template",
"rgb_cmd_t": "rgb_command_topic",
@ -242,6 +245,7 @@ ABBREVIATIONS = {
"tilt_opt": "tilt_optimistic",
"tilt_status_t": "tilt_status_topic",
"tilt_status_tpl": "tilt_status_template",
"tit": "title",
"t": "topic",
"uniq_id": "unique_id",
"unit_of_meas": "unit_of_measurement",

View file

@ -19,6 +19,7 @@ from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_VALUE_TEMPLAT
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_loads
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@ -30,6 +31,7 @@ from .const import (
CONF_QOS,
CONF_RETAIN,
CONF_STATE_TOPIC,
PAYLOAD_EMPTY_JSON,
)
from .debug_info import log_messages
from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper
@ -40,20 +42,28 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "MQTT Update"
CONF_ENTITY_PICTURE = "entity_picture"
CONF_LATEST_VERSION_TEMPLATE = "latest_version_template"
CONF_LATEST_VERSION_TOPIC = "latest_version_topic"
CONF_PAYLOAD_INSTALL = "payload_install"
CONF_RELEASE_SUMMARY = "release_summary"
CONF_RELEASE_URL = "release_url"
CONF_TITLE = "title"
PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend(
{
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_ENTITY_PICTURE): cv.string,
vol.Optional(CONF_LATEST_VERSION_TEMPLATE): cv.template,
vol.Required(CONF_LATEST_VERSION_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_LATEST_VERSION_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PAYLOAD_INSTALL): cv.string,
vol.Optional(CONF_RELEASE_SUMMARY): cv.string,
vol.Optional(CONF_RELEASE_URL): cv.string,
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
vol.Optional(CONF_TITLE): cv.string,
},
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
@ -99,10 +109,22 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
"""Initialize the MQTT update."""
self._config = config
self._attr_device_class = self._config.get(CONF_DEVICE_CLASS)
self._attr_release_summary = self._config.get(CONF_RELEASE_SUMMARY)
self._attr_release_url = self._config.get(CONF_RELEASE_URL)
self._attr_title = self._config.get(CONF_TITLE)
self._entity_picture: str | None = self._config.get(CONF_ENTITY_PICTURE)
UpdateEntity.__init__(self)
MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
@property
def entity_picture(self) -> str | None:
"""Return the entity picture to use in the frontend."""
if self._entity_picture is not None:
return self._entity_picture
return super().entity_picture
@staticmethod
def config_schema() -> vol.Schema:
"""Return the config schema."""
@ -138,15 +160,59 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
@callback
@log_messages(self.hass, self.entity_id)
def handle_installed_version_received(msg: ReceiveMessage) -> None:
"""Handle receiving installed version via MQTT."""
installed_version = self._templates[CONF_VALUE_TEMPLATE](msg.payload)
def handle_state_message_received(msg: ReceiveMessage) -> None:
"""Handle receiving state message via MQTT."""
payload = self._templates[CONF_VALUE_TEMPLATE](msg.payload)
if isinstance(installed_version, str) and installed_version != "":
self._attr_installed_version = installed_version
if not payload or payload == PAYLOAD_EMPTY_JSON:
_LOGGER.debug(
"Ignoring empty payload '%s' after rendering for topic %s",
payload,
msg.topic,
)
return
json_payload = {}
try:
json_payload = json_loads(payload)
_LOGGER.debug(
"JSON payload detected after processing payload '%s' on topic %s",
json_payload,
msg.topic,
)
except JSON_DECODE_EXCEPTIONS:
_LOGGER.warning(
"No valid (JSON) payload detected after processing payload '%s' on topic %s",
json_payload,
msg.topic,
)
json_payload["installed_version"] = payload
if "installed_version" in json_payload:
self._attr_installed_version = json_payload["installed_version"]
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
add_subscription(topics, CONF_STATE_TOPIC, handle_installed_version_received)
if "latest_version" in json_payload:
self._attr_latest_version = json_payload["latest_version"]
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
if CONF_TITLE in json_payload and not self._attr_title:
self._attr_title = json_payload[CONF_TITLE]
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
if CONF_RELEASE_SUMMARY in json_payload and not self._attr_release_summary:
self._attr_release_summary = json_payload[CONF_RELEASE_SUMMARY]
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
if CONF_RELEASE_URL in json_payload and not self._attr_release_url:
self._attr_release_url = json_payload[CONF_RELEASE_URL]
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
if CONF_ENTITY_PICTURE in json_payload and not self._entity_picture:
self._entity_picture = json_payload[CONF_ENTITY_PICTURE]
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
add_subscription(topics, CONF_STATE_TOPIC, handle_state_message_received)
@callback
@log_messages(self.hass, self.entity_id)

View file

@ -6,7 +6,13 @@ import pytest
from homeassistant.components import mqtt, update
from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform
from homeassistant.const import (
ATTR_ENTITY_ID,
STATE_OFF,
STATE_ON,
STATE_UNKNOWN,
Platform,
)
from homeassistant.setup import async_setup_component
from .test_common import (
@ -68,6 +74,10 @@ async def test_run_update_setup(hass, mqtt_mock_entry_with_yaml_config):
"state_topic": installed_version_topic,
"latest_version_topic": latest_version_topic,
"name": "Test Update",
"release_summary": "Test release summary",
"release_url": "https://example.com/release",
"title": "Test Update Title",
"entity_picture": "https://example.com/icon.png",
}
}
},
@ -84,6 +94,10 @@ async def test_run_update_setup(hass, mqtt_mock_entry_with_yaml_config):
assert state.state == STATE_OFF
assert state.attributes.get("installed_version") == "1.9.0"
assert state.attributes.get("latest_version") == "1.9.0"
assert state.attributes.get("release_summary") == "Test release summary"
assert state.attributes.get("release_url") == "https://example.com/release"
assert state.attributes.get("title") == "Test Update Title"
assert state.attributes.get("entity_picture") == "https://example.com/icon.png"
async_fire_mqtt_message(hass, latest_version_topic, "2.0.0")
@ -126,6 +140,10 @@ async def test_value_template(hass, mqtt_mock_entry_with_yaml_config):
assert state.state == STATE_OFF
assert state.attributes.get("installed_version") == "1.9.0"
assert state.attributes.get("latest_version") == "1.9.0"
assert (
state.attributes.get("entity_picture")
== "https://brands.home-assistant.io/_/mqtt/icon.png"
)
async_fire_mqtt_message(hass, latest_version_topic, '{"latest":"2.0.0"}')
@ -137,6 +155,120 @@ async def test_value_template(hass, mqtt_mock_entry_with_yaml_config):
assert state.attributes.get("latest_version") == "2.0.0"
async def test_empty_json_state_message(hass, mqtt_mock_entry_with_yaml_config):
"""Test an empty JSON payload."""
state_topic = "test/state-topic"
await async_setup_component(
hass,
mqtt.DOMAIN,
{
mqtt.DOMAIN: {
update.DOMAIN: {
"state_topic": state_topic,
"name": "Test Update",
}
}
},
)
await hass.async_block_till_done()
await mqtt_mock_entry_with_yaml_config()
async_fire_mqtt_message(hass, state_topic, "{}")
await hass.async_block_till_done()
state = hass.states.get("update.test_update")
assert state.state == STATE_UNKNOWN
async def test_json_state_message(hass, mqtt_mock_entry_with_yaml_config):
"""Test whether it fetches data from a JSON payload."""
state_topic = "test/state-topic"
await async_setup_component(
hass,
mqtt.DOMAIN,
{
mqtt.DOMAIN: {
update.DOMAIN: {
"state_topic": state_topic,
"name": "Test Update",
}
}
},
)
await hass.async_block_till_done()
await mqtt_mock_entry_with_yaml_config()
async_fire_mqtt_message(
hass,
state_topic,
'{"installed_version":"1.9.0","latest_version":"1.9.0",'
'"title":"Test Update Title","release_url":"https://example.com/release",'
'"release_summary":"Test release summary"}',
)
await hass.async_block_till_done()
state = hass.states.get("update.test_update")
assert state.state == STATE_OFF
assert state.attributes.get("installed_version") == "1.9.0"
assert state.attributes.get("latest_version") == "1.9.0"
assert state.attributes.get("release_summary") == "Test release summary"
assert state.attributes.get("release_url") == "https://example.com/release"
assert state.attributes.get("title") == "Test Update Title"
async_fire_mqtt_message(
hass,
state_topic,
'{"installed_version":"1.9.0","latest_version":"2.0.0","title":"Test Update Title"}',
)
await hass.async_block_till_done()
state = hass.states.get("update.test_update")
assert state.state == STATE_ON
assert state.attributes.get("installed_version") == "1.9.0"
assert state.attributes.get("latest_version") == "2.0.0"
async def test_json_state_message_with_template(hass, mqtt_mock_entry_with_yaml_config):
"""Test whether it fetches data from a JSON payload with template."""
state_topic = "test/state-topic"
await async_setup_component(
hass,
mqtt.DOMAIN,
{
mqtt.DOMAIN: {
update.DOMAIN: {
"state_topic": state_topic,
"value_template": '{{ {"installed_version": value_json.installed, "latest_version": value_json.latest} | to_json }}',
"name": "Test Update",
}
}
},
)
await hass.async_block_till_done()
await mqtt_mock_entry_with_yaml_config()
async_fire_mqtt_message(hass, state_topic, '{"installed":"1.9.0","latest":"1.9.0"}')
await hass.async_block_till_done()
state = hass.states.get("update.test_update")
assert state.state == STATE_OFF
assert state.attributes.get("installed_version") == "1.9.0"
assert state.attributes.get("latest_version") == "1.9.0"
async_fire_mqtt_message(hass, state_topic, '{"installed":"1.9.0","latest":"2.0.0"}')
await hass.async_block_till_done()
state = hass.states.get("update.test_update")
assert state.state == STATE_ON
assert state.attributes.get("installed_version") == "1.9.0"
assert state.attributes.get("latest_version") == "2.0.0"
async def test_run_install_service(hass, mqtt_mock_entry_with_yaml_config):
"""Test that install service works."""
installed_version_topic = "test/installed-version"