Home Assistant Unofficial Reference 2024.12.1
update.py
Go to the documentation of this file.
1 """Configure update platform in a device through MQTT topic."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 
8 import voluptuous as vol
9 
10 from homeassistant.components import update
12  DEVICE_CLASSES_SCHEMA,
13  UpdateEntity,
14  UpdateEntityFeature,
15 )
16 from homeassistant.config_entries import ConfigEntry
17 from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_VALUE_TEMPLATE
18 from homeassistant.core import HomeAssistant, callback
19 from homeassistant.helpers import config_validation as cv
20 from homeassistant.helpers.entity_platform import AddEntitiesCallback
21 from homeassistant.helpers.restore_state import RestoreEntity
22 from homeassistant.helpers.typing import ConfigType, VolSchemaType
23 from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads
24 
25 from . import subscription
26 from .config import DEFAULT_RETAIN, MQTT_RO_SCHEMA
27 from .const import CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_STATE_TOPIC, PAYLOAD_EMPTY_JSON
28 from .entity import MqttEntity, async_setup_entity_entry_helper
29 from .models import MqttValueTemplate, ReceiveMessage
30 from .schemas import MQTT_ENTITY_COMMON_SCHEMA
31 from .util import valid_publish_topic, valid_subscribe_topic
32 
33 _LOGGER = logging.getLogger(__name__)
34 
35 PARALLEL_UPDATES = 0
36 
37 DEFAULT_NAME = "MQTT Update"
38 
39 CONF_DISPLAY_PRECISION = "display_precision"
40 CONF_LATEST_VERSION_TEMPLATE = "latest_version_template"
41 CONF_LATEST_VERSION_TOPIC = "latest_version_topic"
42 CONF_PAYLOAD_INSTALL = "payload_install"
43 CONF_RELEASE_SUMMARY = "release_summary"
44 CONF_RELEASE_URL = "release_url"
45 CONF_TITLE = "title"
46 
47 
48 PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend(
49  {
50  vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
51  vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
52  vol.Optional(CONF_DISPLAY_PRECISION, default=0): cv.positive_int,
53  vol.Optional(CONF_LATEST_VERSION_TEMPLATE): cv.template,
54  vol.Optional(CONF_LATEST_VERSION_TOPIC): valid_subscribe_topic,
55  vol.Optional(CONF_NAME): vol.Any(cv.string, None),
56  vol.Optional(CONF_PAYLOAD_INSTALL): cv.string,
57  vol.Optional(CONF_RELEASE_SUMMARY): cv.string,
58  vol.Optional(CONF_RELEASE_URL): cv.string,
59  vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
60  vol.Optional(CONF_TITLE): cv.string,
61  },
62 ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
63 
64 
65 DISCOVERY_SCHEMA = vol.All(PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA))
66 
67 
68 MQTT_JSON_UPDATE_SCHEMA = vol.Schema(
69  {
70  vol.Optional("installed_version"): cv.string,
71  vol.Optional("latest_version"): cv.string,
72  vol.Optional("title"): cv.string,
73  vol.Optional("release_summary"): cv.string,
74  vol.Optional("release_url"): cv.url,
75  vol.Optional("entity_picture"): cv.url,
76  vol.Optional("in_progress"): cv.boolean,
77  vol.Optional("update_percentage"): vol.Any(vol.Range(min=0, max=100), None),
78  }
79 )
80 
81 
83  hass: HomeAssistant,
84  config_entry: ConfigEntry,
85  async_add_entities: AddEntitiesCallback,
86 ) -> None:
87  """Set up MQTT update entity through YAML and through MQTT discovery."""
89  hass,
90  config_entry,
91  MqttUpdate,
92  update.DOMAIN,
93  async_add_entities,
94  DISCOVERY_SCHEMA,
95  PLATFORM_SCHEMA_MODERN,
96  )
97 
98 
100  """Representation of the MQTT update entity."""
101 
102  _default_name = DEFAULT_NAME
103  _entity_id_format = update.ENTITY_ID_FORMAT
104 
105  @property
106  def entity_picture(self) -> str | None:
107  """Return the entity picture to use in the frontend."""
108  if self._attr_entity_picture_attr_entity_picture_attr_entity_picture is not None:
109  return self._attr_entity_picture_attr_entity_picture_attr_entity_picture
110 
111  return super().entity_picture
112 
113  @staticmethod
114  def config_schema() -> VolSchemaType:
115  """Return the config schema."""
116  return DISCOVERY_SCHEMA
117 
118  def _setup_from_config(self, config: ConfigType) -> None:
119  """(Re)Setup the entity."""
120  self._attr_device_class_attr_device_class = self._config_config.get(CONF_DEVICE_CLASS)
121  self._attr_display_precision_attr_display_precision = self._config_config[CONF_DISPLAY_PRECISION]
122  self._attr_release_summary_attr_release_summary = self._config_config.get(CONF_RELEASE_SUMMARY)
123  self._attr_release_url_attr_release_url = self._config_config.get(CONF_RELEASE_URL)
124  self._attr_title_attr_title = self._config_config.get(CONF_TITLE)
125  self._templates_templates = {
126  CONF_VALUE_TEMPLATE: MqttValueTemplate(
127  config.get(CONF_VALUE_TEMPLATE),
128  entity=self,
129  ).async_render_with_possible_json_value,
130  CONF_LATEST_VERSION_TEMPLATE: MqttValueTemplate(
131  config.get(CONF_LATEST_VERSION_TEMPLATE),
132  entity=self,
133  ).async_render_with_possible_json_value,
134  }
135 
136  @callback
137  def _handle_state_message_received(self, msg: ReceiveMessage) -> None:
138  """Handle receiving state message via MQTT."""
139  payload = self._templates_templates[CONF_VALUE_TEMPLATE](msg.payload)
140 
141  if not payload or payload == PAYLOAD_EMPTY_JSON:
142  _LOGGER.debug(
143  "Ignoring empty payload '%s' after rendering for topic %s",
144  payload,
145  msg.topic,
146  )
147  return
148 
149  json_payload: dict[str, Any] = {}
150  try:
151  rendered_json_payload = json_loads(payload)
152  if isinstance(rendered_json_payload, dict):
153  _LOGGER.debug(
154  (
155  "JSON payload detected after processing payload '%s' on"
156  " topic %s"
157  ),
158  rendered_json_payload,
159  msg.topic,
160  )
161  json_payload = MQTT_JSON_UPDATE_SCHEMA(rendered_json_payload)
162  else:
163  _LOGGER.debug(
164  (
165  "Non-dictionary JSON payload detected after processing"
166  " payload '%s' on topic %s"
167  ),
168  payload,
169  msg.topic,
170  )
171  json_payload = {"installed_version": str(payload)}
172  except vol.MultipleInvalid as exc:
173  _LOGGER.warning(
174  (
175  "Schema violation after processing payload '%s'"
176  " on topic '%s' for entity '%s': %s"
177  ),
178  payload,
179  msg.topic,
180  self.entity_identity_id,
181  exc,
182  )
183  return
184  except JSON_DECODE_EXCEPTIONS:
185  _LOGGER.debug(
186  (
187  "No valid (JSON) payload detected after processing payload '%s'"
188  " on topic '%s' for entity '%s'"
189  ),
190  payload,
191  msg.topic,
192  self.entity_identity_id,
193  )
194  json_payload["installed_version"] = str(payload)
195 
196  if "installed_version" in json_payload:
197  self._attr_installed_version_attr_installed_version = json_payload["installed_version"]
198 
199  if "latest_version" in json_payload:
200  self._attr_latest_version_attr_latest_version = json_payload["latest_version"]
201 
202  if "title" in json_payload:
203  self._attr_title_attr_title = json_payload["title"]
204 
205  if "release_summary" in json_payload:
206  self._attr_release_summary_attr_release_summary = json_payload["release_summary"]
207 
208  if "release_url" in json_payload:
209  self._attr_release_url_attr_release_url = json_payload["release_url"]
210 
211  if "entity_picture" in json_payload:
212  self._attr_entity_picture_attr_entity_picture_attr_entity_picture = json_payload["entity_picture"]
213 
214  if "update_percentage" in json_payload:
215  self._attr_update_percentage_attr_update_percentage = json_payload["update_percentage"]
216  self._attr_in_progress_attr_in_progress_attr_in_progress = self._attr_update_percentage_attr_update_percentage is not None
217 
218  if "in_progress" in json_payload:
219  self._attr_in_progress_attr_in_progress_attr_in_progress = json_payload["in_progress"]
220 
221  @callback
222  def _handle_latest_version_received(self, msg: ReceiveMessage) -> None:
223  """Handle receiving latest version via MQTT."""
224  latest_version = self._templates_templates[CONF_LATEST_VERSION_TEMPLATE](msg.payload)
225 
226  if isinstance(latest_version, str) and latest_version != "":
227  self._attr_latest_version_attr_latest_version = latest_version
228 
229  @callback
230  def _prepare_subscribe_topics(self) -> None:
231  """(Re)Subscribe to topics."""
232  self.add_subscriptionadd_subscription(
233  CONF_STATE_TOPIC,
234  self._handle_state_message_received_handle_state_message_received,
235  {
236  "_attr_entity_picture",
237  "_attr_in_progress",
238  "_attr_installed_version",
239  "_attr_latest_version",
240  "_attr_title",
241  "_attr_release_summary",
242  "_attr_release_url",
243  "_attr_update_percentage",
244  },
245  )
246  self.add_subscriptionadd_subscription(
247  CONF_LATEST_VERSION_TOPIC,
248  self._handle_latest_version_received_handle_latest_version_received,
249  {"_attr_latest_version"},
250  )
251 
252  async def _subscribe_topics(self) -> None:
253  """(Re)Subscribe to topics."""
254  subscription.async_subscribe_topics_internal(self.hasshasshass, self._sub_state_sub_state)
255 
256  async def async_install(
257  self, version: str | None, backup: bool, **kwargs: Any
258  ) -> None:
259  """Update the current value."""
260  payload = self._config_config[CONF_PAYLOAD_INSTALL]
261  await self.async_publish_with_configasync_publish_with_config(self._config_config[CONF_COMMAND_TOPIC], payload)
262 
263  @property
264  def supported_features(self) -> UpdateEntityFeature:
265  """Return the list of supported features."""
266  support = UpdateEntityFeature(UpdateEntityFeature.PROGRESS)
267 
268  if self._config_config.get(CONF_COMMAND_TOPIC) is not None:
269  support |= UpdateEntityFeature.INSTALL
270 
271  return support
None async_publish_with_config(self, str topic, PublishPayloadType payload)
Definition: entity.py:1377
bool add_subscription(self, str state_topic_config_key, Callable[[ReceiveMessage], None] msg_callback, set[str]|None tracked_attributes, bool disable_encoding=False)
Definition: entity.py:1484
None _handle_state_message_received(self, ReceiveMessage msg)
Definition: update.py:137
UpdateEntityFeature supported_features(self)
Definition: update.py:264
None async_install(self, str|None version, bool backup, **Any kwargs)
Definition: update.py:258
None _setup_from_config(self, ConfigType config)
Definition: update.py:118
None _handle_latest_version_received(self, ReceiveMessage msg)
Definition: update.py:222
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_setup_entity_entry_helper(HomeAssistant hass, ConfigEntry entry, type[MqttEntity]|None entity_class, str domain, AddEntitiesCallback async_add_entities, VolSchemaType discovery_schema, VolSchemaType platform_schema_modern, dict[str, type[MqttEntity]]|None schema_class_mapping=None)
Definition: entity.py:245
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: update.py:86