mirror of
https://github.com/home-assistant/core.git
synced 2026-09-24 07:25:52 -05:00
Add mqtt image subentry support (#151586)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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%]",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}},
|
||||
|
||||
Reference in New Issue
Block a user