Improve MQTT update platform (#81131)
* 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:
parent
9b87f7f6f9
commit
4684101a85
3 changed files with 211 additions and 9 deletions
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue