Home Assistant Unofficial Reference 2024.12.1
image.py
Go to the documentation of this file.
1 """Support for MQTT images."""
2 
3 from __future__ import annotations
4 
5 from base64 import b64decode
6 import binascii
7 from collections.abc import Callable
8 import logging
9 from typing import TYPE_CHECKING, Any
10 
11 import httpx
12 import voluptuous as vol
13 
14 from homeassistant.components import image
15 from homeassistant.components.image import DEFAULT_CONTENT_TYPE, ImageEntity
16 from homeassistant.config_entries import ConfigEntry
17 from homeassistant.const import CONF_NAME
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.httpx_client import get_async_client
22 from homeassistant.helpers.service_info.mqtt import ReceivePayloadType
23 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolSchemaType
24 from homeassistant.util import dt as dt_util
25 
26 from . import subscription
27 from .config import MQTT_BASE_SCHEMA
28 from .entity import MqttEntity, async_setup_entity_entry_helper
29 from .models import (
30  DATA_MQTT,
31  MqttValueTemplate,
32  MqttValueTemplateException,
33  ReceiveMessage,
34 )
35 from .schemas import MQTT_ENTITY_COMMON_SCHEMA
36 from .util import valid_subscribe_topic
37 
38 _LOGGER = logging.getLogger(__name__)
39 
40 PARALLEL_UPDATES = 0
41 
42 CONF_CONTENT_TYPE = "content_type"
43 CONF_IMAGE_ENCODING = "image_encoding"
44 CONF_IMAGE_TOPIC = "image_topic"
45 CONF_URL_TEMPLATE = "url_template"
46 CONF_URL_TOPIC = "url_topic"
47 
48 DEFAULT_NAME = "MQTT Image"
49 
50 GET_IMAGE_TIMEOUT = 10
51 
52 
53 def validate_topic_required(config: ConfigType) -> ConfigType:
54  """Ensure at least one subscribe topic is configured."""
55  if CONF_IMAGE_TOPIC not in config and CONF_URL_TOPIC not in config:
56  raise vol.Invalid("Expected one of [`image_topic`, `url_topic`], got none")
57  if CONF_CONTENT_TYPE in config and CONF_URL_TOPIC in config:
58  raise vol.Invalid(
59  "Option `content_type` can not be used together with `url_topic`"
60  )
61  return config
62 
63 
64 PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend(
65  {
66  vol.Optional(CONF_CONTENT_TYPE): cv.string,
67  vol.Optional(CONF_NAME): vol.Any(cv.string, None),
68  vol.Exclusive(CONF_URL_TOPIC, "image_topic"): valid_subscribe_topic,
69  vol.Exclusive(CONF_IMAGE_TOPIC, "image_topic"): valid_subscribe_topic,
70  vol.Optional(CONF_IMAGE_ENCODING): "b64",
71  vol.Optional(CONF_URL_TEMPLATE): cv.template,
72  }
73 ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
74 
75 PLATFORM_SCHEMA_MODERN = vol.All(PLATFORM_SCHEMA_BASE.schema, validate_topic_required)
76 
77 DISCOVERY_SCHEMA = vol.All(
78  PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), validate_topic_required
79 )
80 
81 
83  hass: HomeAssistant,
84  config_entry: ConfigEntry,
85  async_add_entities: AddEntitiesCallback,
86 ) -> None:
87  """Set up MQTT image through YAML and through MQTT discovery."""
89  hass,
90  config_entry,
91  MqttImage,
92  image.DOMAIN,
93  async_add_entities,
94  DISCOVERY_SCHEMA,
95  PLATFORM_SCHEMA_MODERN,
96  )
97 
98 
100  """representation of a MQTT image."""
101 
102  _default_name = DEFAULT_NAME
103  _entity_id_format: str = image.ENTITY_ID_FORMAT
104  _last_image: bytes | None = None
105  _client: httpx.AsyncClient
106  _url_template: Callable[[ReceivePayloadType], ReceivePayloadType]
107  _topic: dict[str, Any]
108 
109  def __init__(
110  self,
111  hass: HomeAssistant,
112  config: ConfigType,
113  config_entry: ConfigEntry,
114  discovery_data: DiscoveryInfoType | None,
115  ) -> None:
116  """Initialize the MQTT Image."""
117  self._client_client_client = get_async_client(hass)
118  ImageEntity.__init__(self, hass)
119  MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
120 
121  @staticmethod
122  def config_schema() -> VolSchemaType:
123  """Return the config schema."""
124  return DISCOVERY_SCHEMA
125 
126  def _setup_from_config(self, config: ConfigType) -> None:
127  """(Re)Setup the entity."""
128  self._topic_topic = {
129  key: config.get(key)
130  for key in (
131  CONF_IMAGE_TOPIC,
132  CONF_URL_TOPIC,
133  )
134  }
135  if CONF_IMAGE_TOPIC in config:
136  self._attr_content_type_attr_content_type_attr_content_type = config.get(
137  CONF_CONTENT_TYPE, DEFAULT_CONTENT_TYPE
138  )
139  if CONF_URL_TOPIC in config:
140  self._attr_image_url_attr_image_url = None
141  self._url_template_url_template = MqttValueTemplate(
142  config.get(CONF_URL_TEMPLATE), entity=self
143  ).async_render_with_possible_json_value
144 
145  @callback
146  def _image_data_received(self, msg: ReceiveMessage) -> None:
147  """Handle new MQTT messages."""
148  try:
149  if CONF_IMAGE_ENCODING in self._config_config:
150  self._last_image_last_image = b64decode(msg.payload)
151  else:
152  if TYPE_CHECKING:
153  assert isinstance(msg.payload, bytes)
154  self._last_image_last_image = msg.payload
155  except (binascii.Error, ValueError, AssertionError) as err:
156  _LOGGER.error(
157  "Error processing image data received at topic %s: %s",
158  msg.topic,
159  err,
160  )
161  self._last_image_last_image = None
162  self._attr_image_last_updated_attr_image_last_updated = dt_util.utcnow()
163  self.hasshasshass.data[DATA_MQTT].state_write_requests.write_state_request(self)
164 
165  @callback
166  def _image_from_url_request_received(self, msg: ReceiveMessage) -> None:
167  """Handle new MQTT messages."""
168  try:
169  url = cv.url(self._url_template_url_template(msg.payload))
170  self._attr_image_url_attr_image_url = url
171  except MqttValueTemplateException as exc:
172  _LOGGER.warning(exc)
173  return
174  except vol.Invalid:
175  _LOGGER.error(
176  "Invalid image URL '%s' received at topic %s",
177  msg.payload,
178  msg.topic,
179  )
180  self._attr_image_last_updated_attr_image_last_updated = dt_util.utcnow()
181  self._cached_image_cached_image_cached_image = None
182  self.hasshasshass.data[DATA_MQTT].state_write_requests.write_state_request(self)
183 
184  @callback
185  def _prepare_subscribe_topics(self) -> None:
186  """(Re)Subscribe to topics."""
187  self.add_subscriptionadd_subscription(
188  CONF_IMAGE_TOPIC, self._image_data_received_image_data_received, None, disable_encoding=True
189  )
190  self.add_subscriptionadd_subscription(
191  CONF_URL_TOPIC, self._image_from_url_request_received_image_from_url_request_received, None
192  )
193 
194  async def _subscribe_topics(self) -> None:
195  """(Re)Subscribe to topics."""
196  subscription.async_subscribe_topics_internal(self.hasshasshass, self._sub_state_sub_state)
197 
198  async def async_image(self) -> bytes | None:
199  """Return bytes of image."""
200  if CONF_IMAGE_TOPIC in self._config_config:
201  return self._last_image_last_image
202  return await super().async_image()
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 __init__(self, HomeAssistant hass, ConfigType config, ConfigEntry config_entry, DiscoveryInfoType|None discovery_data)
Definition: image.py:115
None _setup_from_config(self, ConfigType config)
Definition: image.py:126
None _image_data_received(self, ReceiveMessage msg)
Definition: image.py:146
None _image_from_url_request_received(self, ReceiveMessage msg)
Definition: image.py:166
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: image.py:86
ConfigType validate_topic_required(ConfigType config)
Definition: image.py:53
httpx.AsyncClient get_async_client(HomeAssistant hass, bool verify_ssl=True)
Definition: httpx_client.py:41