Add mqtt image subentry support (#151586)

This commit is contained in:
Jan Bouwhuis
2025-09-23 16:31:40 +02:00
committed by GitHub
parent c867026bdd
commit f6b8aa893b
6 changed files with 194 additions and 8 deletions
@@ -39,6 +39,7 @@ from homeassistant.components.climate import (
from homeassistant.components.cover import CoverDeviceClass
from homeassistant.components.file_upload import process_uploaded_file
from homeassistant.components.hassio import AddonError, AddonManager, AddonState
from homeassistant.components.image import DEFAULT_CONTENT_TYPE
from homeassistant.components.light import (
DEFAULT_MAX_KELVIN,
DEFAULT_MIN_KELVIN,
@@ -167,6 +168,7 @@ from .const import (
CONF_COMMAND_ON_TEMPLATE,
CONF_COMMAND_TEMPLATE,
CONF_COMMAND_TOPIC,
CONF_CONTENT_TYPE,
CONF_CURRENT_HUMIDITY_TEMPLATE,
CONF_CURRENT_HUMIDITY_TOPIC,
CONF_CURRENT_TEMP_TEMPLATE,
@@ -205,6 +207,8 @@ from .const import (
CONF_HUMIDITY_MIN,
CONF_HUMIDITY_STATE_TEMPLATE,
CONF_HUMIDITY_STATE_TOPIC,
CONF_IMAGE_ENCODING,
CONF_IMAGE_TOPIC,
CONF_KEEPALIVE,
CONF_LAST_RESET_VALUE_TEMPLATE,
CONF_MAX_KELVIN,
@@ -330,6 +334,8 @@ from .const import (
CONF_TLS_INSECURE,
CONF_TRANSITION,
CONF_TRANSPORT,
CONF_URL_TEMPLATE,
CONF_URL_TOPIC,
CONF_WHITE_COMMAND_TOPIC,
CONF_WHITE_SCALE,
CONF_WILL_MESSAGE,
@@ -434,6 +440,7 @@ SUBENTRY_PLATFORMS = [
Platform.CLIMATE,
Platform.COVER,
Platform.FAN,
Platform.IMAGE,
Platform.LIGHT,
Platform.LOCK,
Platform.NOTIFY,
@@ -620,6 +627,43 @@ HUMIDITY_SELECTOR = vol.All(
),
vol.Coerce(int),
)
IMAGE_CONTENT_TYPE_SELECTOR = SelectSelector(
SelectSelectorConfig(
options=[
SelectOptionDict(
value="image/jpeg", label="Joint Photographic Expert Group image (JPEG)"
),
SelectOptionDict(
value="image/png", label="Portable Network Graphics (PNG)"
),
SelectOptionDict(
value="image/apng", label="Animated Portable Network Graphics (APNG)"
),
SelectOptionDict(value="image/avif", label="AV1 Image File Format (AVIF)"),
SelectOptionDict(
value="image/gif", label="Graphics Interchange Format (GIF)"
),
SelectOptionDict(
value="image/svg+xml", label="Scalable Vector Graphics (SVG)"
),
SelectOptionDict(value="image/webp", label="Web Picture format (WEBP)"),
],
mode=SelectSelectorMode.DROPDOWN,
)
)
IMAGE_ENCODING_SELECTOR = SelectSelector(
SelectSelectorConfig(
options=["raw", "b64"],
translation_key="image_encoding",
mode=SelectSelectorMode.DROPDOWN,
)
)
IMAGE_PROCESSING_MODE_SELECTOR = SelectSelector(
SelectSelectorConfig(
options=["image_url", "image_data"],
translation_key="image_processing_mode",
)
)
KELVIN_SELECTOR = NumberSelector(
NumberSelectorConfig(
mode=NumberSelectorMode.BOX,
@@ -1019,6 +1063,7 @@ ENTITY_CONFIG_VALIDATOR: dict[
Platform.CLIMATE.value: validate_climate_platform_config,
Platform.COVER.value: validate_cover_platform_config,
Platform.FAN.value: validate_fan_platform_config,
Platform.IMAGE.value: None,
Platform.LIGHT.value: validate_light_platform_config,
Platform.LOCK.value: None,
Platform.NOTIFY.value: None,
@@ -1209,6 +1254,18 @@ PLATFORM_ENTITY_FIELDS: dict[str, dict[str, PlatformField]] = {
default=lambda config: bool(config.get(CONF_DIRECTION_COMMAND_TOPIC)),
),
},
Platform.IMAGE.value: {
"image_processing_mode": PlatformField(
selector=IMAGE_PROCESSING_MODE_SELECTOR,
required=True,
exclude_from_config=True,
default=(
lambda config: "image_url"
if config.get(CONF_IMAGE_TOPIC) is None
else "image_data"
),
)
},
Platform.LIGHT.value: {
CONF_SCHEMA: PlatformField(
selector=LIGHT_SCHEMA_SELECTOR,
@@ -2292,6 +2349,40 @@ PLATFORM_MQTT_FIELDS: dict[str, dict[str, PlatformField]] = {
conditions=({"fan_feature_direction": True},),
),
},
Platform.IMAGE.value: {
CONF_IMAGE_TOPIC: PlatformField(
selector=TEXT_SELECTOR,
required=True,
validator=valid_subscribe_topic,
error="invalid_subscribe_topic",
conditions=({"image_processing_mode": "image_data"},),
),
CONF_CONTENT_TYPE: PlatformField(
selector=IMAGE_CONTENT_TYPE_SELECTOR,
required=True,
default=DEFAULT_CONTENT_TYPE,
conditions=({"image_processing_mode": "image_data"},),
),
CONF_IMAGE_ENCODING: PlatformField(
selector=IMAGE_ENCODING_SELECTOR,
required=False,
conditions=({"image_processing_mode": "image_data"},),
default="raw",
),
CONF_URL_TOPIC: PlatformField(
selector=TEXT_SELECTOR,
required=True,
validator=valid_subscribe_topic,
error="invalid_subscribe_topic",
conditions=({"image_processing_mode": "image_url"},),
),
CONF_URL_TEMPLATE: PlatformField(
selector=TEMPLATE_SELECTOR,
required=False,
validator=validate(cv.template),
error="invalid_template",
),
},
Platform.LIGHT.value: {
CONF_COMMAND_TOPIC: PlatformField(
selector=TEXT_SELECTOR,
+5
View File
@@ -38,9 +38,12 @@ CONF_CODE_FORMAT = "code_format"
CONF_CODE_TRIGGER_REQUIRED = "code_trigger_required"
CONF_COMMAND_TEMPLATE = "command_template"
CONF_COMMAND_TOPIC = "command_topic"
CONF_CONTENT_TYPE = "content_type"
CONF_DEFAULT_ENTITY_ID = "default_entity_id"
CONF_DISCOVERY_PREFIX = "discovery_prefix"
CONF_ENCODING = "encoding"
CONF_IMAGE_ENCODING = "image_encoding"
CONF_IMAGE_TOPIC = "image_topic"
CONF_JSON_ATTRS_TOPIC = "json_attributes_topic"
CONF_JSON_ATTRS_TEMPLATE = "json_attributes_template"
CONF_KEEPALIVE = "keepalive"
@@ -231,6 +234,8 @@ CONF_TILT_MIN = "tilt_min"
CONF_TILT_OPEN_POSITION = "tilt_opened_value"
CONF_TILT_STATE_OPTIMISTIC = "tilt_optimistic"
CONF_TRANSITION = "transition"
CONF_URL_TEMPLATE = "url_template"
CONF_URL_TOPIC = "url_topic"
CONF_XY_COMMAND_TEMPLATE = "xy_command_template"
CONF_XY_COMMAND_TOPIC = "xy_command_topic"
CONF_XY_STATE_TOPIC = "xy_state_topic"
+9 -8
View File
@@ -25,6 +25,13 @@ from homeassistant.util import dt as dt_util
from . import subscription
from .config import MQTT_BASE_SCHEMA
from .const import (
CONF_CONTENT_TYPE,
CONF_IMAGE_ENCODING,
CONF_IMAGE_TOPIC,
CONF_URL_TEMPLATE,
CONF_URL_TOPIC,
)
from .entity import MqttEntity, async_setup_entity_entry_helper
from .models import (
DATA_MQTT,
@@ -39,12 +46,6 @@ _LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
CONF_CONTENT_TYPE = "content_type"
CONF_IMAGE_ENCODING = "image_encoding"
CONF_IMAGE_TOPIC = "image_topic"
CONF_URL_TEMPLATE = "url_template"
CONF_URL_TOPIC = "url_topic"
DEFAULT_NAME = "MQTT Image"
GET_IMAGE_TIMEOUT = 10
@@ -67,7 +68,7 @@ PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend(
vol.Optional(CONF_NAME): vol.Any(cv.string, None),
vol.Exclusive(CONF_URL_TOPIC, "image_topic"): valid_subscribe_topic,
vol.Exclusive(CONF_IMAGE_TOPIC, "image_topic"): valid_subscribe_topic,
vol.Optional(CONF_IMAGE_ENCODING): "b64",
vol.Optional(CONF_IMAGE_ENCODING): vol.In({"b64", "raw"}),
vol.Optional(CONF_URL_TEMPLATE): cv.template,
}
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
@@ -146,7 +147,7 @@ class MqttImage(MqttEntity, ImageEntity):
def _image_data_received(self, msg: ReceiveMessage) -> None:
"""Handle new MQTT messages."""
try:
if CONF_IMAGE_ENCODING in self._config:
if self._config.get(CONF_IMAGE_ENCODING) == "b64":
self._last_image = b64decode(msg.payload)
else:
if TYPE_CHECKING:
@@ -264,6 +264,7 @@
"fan_feature_preset_modes": "Preset modes support",
"fan_feature_oscillation": "Oscillation support",
"fan_feature_direction": "Direction support",
"image_processing_mode": "Image processing mode",
"options": "Add option",
"schema": "Schema",
"state_class": "State class",
@@ -290,6 +291,7 @@
"fan_feature_preset_modes": "The fan supports preset modes.",
"fan_feature_oscillation": "The fan supports oscillation.",
"fan_feature_direction": "The fan supports direction.",
"image_processing_mode": "Select how the image data is received.",
"options": "Options for allowed sensor state values. The sensor’s Device class must be set to Enumeration. The 'Options' setting cannot be used together with State class or Unit of measurement.",
"schema": "The schema to use. [Learn more.]({url}#comparison-of-light-mqtt-schemas)",
"state_class": "The [State class](https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes) of the sensor. [Learn more.]({url}#state_class)",
@@ -326,8 +328,11 @@
"command_topic": "Command topic",
"command_off_template": "Command \"off\" template",
"command_on_template": "Command \"on\" template",
"content_type": "Content type",
"force_update": "Force update",
"green_template": "Green template",
"image_encoding": "Image encoding",
"image_topic": "Image topic",
"last_reset_value_template": "Last reset value template",
"modes": "Supported operation modes",
"mode_command_topic": "Operation mode command topic",
@@ -348,6 +353,8 @@
"state_topic": "State topic",
"state_value_template": "State value template",
"supported_color_modes": "Supported color modes",
"url_template": "URL template",
"url_topic": "URL topic",
"value_template": "Value template"
},
"data_description": {
@@ -363,8 +370,11 @@
"command_on_template": "The [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) for \"on\" state changes. Available variables: `state`, `brightness`, `color_temp`, `red`, `green`, `blue`, `hue`, `sat`, `flash`, `transition` and `effect`. Values `red`, `green`, `blue` and `brightness` are provided as integers from range 0-255. Value of `hue` is provided as float from range 0-360. Value of `sat` is provided as float from range 0-100. Value of `color_temp` is provided as integer representing Kelvin units.",
"command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to render the payload to be published at the command topic. [Learn more.]({url}#command_template)",
"command_topic": "The publishing topic that will be used to control the {platform} entity. [Learn more.]({url}#command_topic)",
"content_type": "The content type or the image data that is received at the image topic.",
"force_update": "Sends update events even if the value hasn’t changed. Useful if you want to have meaningful value graphs in history. [Learn more.]({url}#force_update)",
"green_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract green color from the state payload value. Expected result of the template is an integer from 0-255 range.",
"image_encoding": "Select the encoding of the received image data",
"image_topic": "The MQTT topic subscribed to receive messages containing the image data. [Learn more.]({url}#image_topic)",
"last_reset_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the last reset. When Last reset template is set, the State class option must be Total. [Learn more.]({url}#last_reset_value_template)",
"modes": "A list of supported operation modes. [Learn more.]({url}#modes)",
"mode_command_topic": "The MQTT topic to publish commands to change the climate operation mode. [Learn more.]({url}#mode_command_topic)",
@@ -384,6 +394,8 @@
"state_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract state from the state payload value.",
"state_topic": "The MQTT topic subscribed to receive {platform} state values. [Learn more.]({url}#state_topic)",
"supported_color_modes": "A list of color modes supported by the light. Possible color modes are On/Off, Brightness, Color temperature, HS, XY, RGB, RGBW, RGBWW, White. Note that if On/Off or Brightness are used, that must be the only value in the list. [Learn more.]({url}#supported_color_modes)",
"url_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract an URL from the received URL topic payload value. [Learn more.]({url}#url_template)",
"url_topic": "The MQTT topic subscribed to receive messages containing the image URL. [Learn more.]({url}#url_topic)",
"value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the {platform} entity value. [Learn more.]({url}#value_template)"
},
"sections": {
@@ -1261,6 +1273,12 @@
"diagnostic": "Diagnostic"
}
},
"image_encoding": {
"options": {
"raw": "Raw data",
"b64": "Base64 encoding"
}
},
"image_processing_mode": {
"options": {
"image_data": "Image data is received",
@@ -1289,6 +1307,7 @@
"climate": "[%key:component::climate::title%]",
"cover": "[%key:component::cover::title%]",
"fan": "[%key:component::fan::title%]",
"image": "[%key:component::image::title%]",
"light": "[%key:component::light::title%]",
"lock": "[%key:component::lock::title%]",
"notify": "[%key:component::notify::title%]",
+29
View File
@@ -356,6 +356,27 @@ MOCK_SUBENTRY_FAN_COMPONENT = {
"speed_range_min": 1,
},
}
MOCK_SUBENTRY_IMAGE_COMPONENT_DATA = {
"24402bcbd5b64a54bc32695a5ef752bf": {
"platform": "image",
"name": "Merchandise",
"entity_category": None,
"image_topic": "test-topic",
"content_type": "image/jpeg",
"image_encoding": "b64",
"entity_picture": "https://example.com/24402bcbd5b64a54bc32695a5ef752bf",
},
}
MOCK_SUBENTRY_IMAGE_COMPONENT_URL = {
"326104eb58af48c9ab1f887cded499bb": {
"platform": "image",
"name": "Merchandise",
"entity_category": None,
"url_topic": "test-topic",
"url_template": "{{ value_json.value }}",
"entity_picture": "https://example.com/326104eb58af48c9ab1f887cded499bb",
},
}
MOCK_SUBENTRY_LIGHT_BASIC_KELVIN_COMPONENT = {
"8131babc5e8d4f44b82e0761d39091a2": {
"platform": "light",
@@ -553,6 +574,14 @@ MOCK_FAN_SUBENTRY_DATA_SINGLE = {
"device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}},
"components": MOCK_SUBENTRY_FAN_COMPONENT,
}
MOCK_IMAGE_SUBENTRY_DATA_IMAGE_DATA = {
"device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}},
"components": MOCK_SUBENTRY_IMAGE_COMPONENT_DATA,
}
MOCK_IMAGE_SUBENTRY_DATA_IMAGE_URL = {
"device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}},
"components": MOCK_SUBENTRY_IMAGE_COMPONENT_URL,
}
MOCK_LIGHT_BASIC_KELVIN_SUBENTRY_DATA_SINGLE = {
"device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}},
"components": MOCK_SUBENTRY_LIGHT_BASIC_KELVIN_COMPONENT,
+41
View File
@@ -43,6 +43,8 @@ from .common import (
MOCK_CLIMATE_SUBENTRY_DATA_SINGLE,
MOCK_COVER_SUBENTRY_DATA_SINGLE,
MOCK_FAN_SUBENTRY_DATA_SINGLE,
MOCK_IMAGE_SUBENTRY_DATA_IMAGE_DATA,
MOCK_IMAGE_SUBENTRY_DATA_IMAGE_URL,
MOCK_LIGHT_BASIC_KELVIN_SUBENTRY_DATA_SINGLE,
MOCK_LOCK_SUBENTRY_DATA_SINGLE,
MOCK_NOTIFY_SUBENTRY_DATA_MULTI,
@@ -3279,6 +3281,45 @@ async def test_migrate_of_incompatible_config_entry(
"Milk notifier Breezer",
id="fan",
),
pytest.param(
MOCK_IMAGE_SUBENTRY_DATA_IMAGE_DATA,
{"name": "Milk notifier", "mqtt_settings": {"qos": 0}},
{"name": "Merchandise"},
{"image_processing_mode": "image_data"},
(),
{
"image_topic": "test-topic",
"content_type": "image/jpeg",
"image_encoding": "b64",
},
(
(
{"image_topic": "test-topic#invalid", "content_type": "image/jpeg"},
{"image_topic": "invalid_subscribe_topic"},
),
),
"Milk notifier Merchandise",
id="notify_image_data",
),
pytest.param(
MOCK_IMAGE_SUBENTRY_DATA_IMAGE_URL,
{"name": "Milk notifier", "mqtt_settings": {"qos": 0}},
{"name": "Merchandise"},
{"image_processing_mode": "image_url"},
(),
{
"url_topic": "test-topic",
"url_template": "{{ value_json.value }}",
},
(
(
{"url_topic": "test-topic#invalid"},
{"url_topic": "invalid_subscribe_topic"},
),
),
"Milk notifier Merchandise",
id="notify_image_url",
),
pytest.param(
MOCK_LIGHT_BASIC_KELVIN_SUBENTRY_DATA_SINGLE,
{"name": "Milk notifier", "mqtt_settings": {"qos": 1}},