diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 257e0fe95ae..8bc318e4897 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -41,6 +41,7 @@ ABBREVIATIONS = { "cod_dis_req": "code_disarm_required", "cod_form": "code_format", "cod_trig_req": "code_trigger_required", + "cont_type": "content_type", "curr_hum_t": "current_humidity_topic", "curr_hum_tpl": "current_humidity_template", "curr_temp_t": "current_temperature_topic", @@ -83,6 +84,7 @@ ABBREVIATIONS = { "hs_val_tpl": "hs_value_template", "ic": "icon", "img_e": "image_encoding", + "img_t": "image_topic", "init": "initial", "hum_cmd_t": "target_humidity_command_topic", "hum_cmd_tpl": "target_humidity_command_template", diff --git a/homeassistant/components/mqtt/config_integration.py b/homeassistant/components/mqtt/config_integration.py index fdc32a601e0..ba2e0427ba7 100644 --- a/homeassistant/components/mqtt/config_integration.py +++ b/homeassistant/components/mqtt/config_integration.py @@ -24,6 +24,7 @@ from . import ( device_tracker as device_tracker_platform, fan as fan_platform, humidifier as humidifier_platform, + image as image_platform, light as light_platform, lock as lock_platform, number as number_platform, @@ -89,6 +90,10 @@ PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( cv.ensure_list, [humidifier_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), + Platform.IMAGE.value: vol.All( + cv.ensure_list, + [image_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] + ), Platform.LOCK.value: vol.All( cv.ensure_list, [lock_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index fd259965d20..bf01bb13483 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -113,6 +113,7 @@ PLATFORMS = [ Platform.COVER, Platform.FAN, Platform.HUMIDIFIER, + Platform.IMAGE, Platform.LIGHT, Platform.LOCK, Platform.NUMBER, @@ -137,6 +138,7 @@ RELOADABLE_PLATFORMS = [ Platform.DEVICE_TRACKER, Platform.FAN, Platform.HUMIDIFIER, + Platform.IMAGE, Platform.LIGHT, Platform.LOCK, Platform.NUMBER, diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 0411a1f679c..70e5ac9e535 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -54,6 +54,7 @@ SUPPORTED_COMPONENTS = [ "device_tracker", "fan", "humidifier", + "image", "light", "lock", "number", diff --git a/homeassistant/components/mqtt/image.py b/homeassistant/components/mqtt/image.py new file mode 100644 index 00000000000..80b0a6be1f6 --- /dev/null +++ b/homeassistant/components/mqtt/image.py @@ -0,0 +1,155 @@ +"""Support for MQTT images.""" +from __future__ import annotations + +from base64 import b64decode +import binascii +from collections.abc import Callable +import functools +import logging +from typing import Any + +import httpx +import voluptuous as vol + +from homeassistant.components import image +from homeassistant.components.image import ( + DEFAULT_CONTENT_TYPE, + ImageEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.helpers.service_info.mqtt import ReceivePayloadType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util import dt as dt_util + +from . import subscription +from .config import MQTT_BASE_SCHEMA +from .const import CONF_QOS +from .debug_info import log_messages +from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper +from .models import ReceiveMessage +from .util import get_mqtt_data, valid_subscribe_topic + +_LOGGER = logging.getLogger(__name__) + +CONF_CONTENT_TYPE = "content_type" +CONF_IMAGE_ENCODING = "image_encoding" +CONF_IMAGE_TOPIC = "image_topic" + +DEFAULT_NAME = "MQTT Image" + + +PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( + { + vol.Optional(CONF_CONTENT_TYPE, default=DEFAULT_CONTENT_TYPE): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_IMAGE_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_IMAGE_ENCODING): "b64", + } +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) + + +DISCOVERY_SCHEMA = vol.All(PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA)) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up MQTT image through YAML and through MQTT discovery.""" + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry + ) + await async_setup_entry_helper(hass, image.DOMAIN, setup, DISCOVERY_SCHEMA) + + +async def _async_setup_entity( + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, +) -> None: + """Set up the MQTT Image.""" + async_add_entities([MqttImage(hass, config, config_entry, discovery_data)]) + + +class MqttImage(MqttEntity, ImageEntity): + """representation of a MQTT image.""" + + _entity_id_format: str = image.ENTITY_ID_FORMAT + _last_image: bytes | None = None + _client: httpx.AsyncClient + _url_template: Callable[[ReceivePayloadType], ReceivePayloadType] + _topic: dict[str, Any] + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: + """Initialize the MQTT Image.""" + self._client = get_async_client(hass) + ImageEntity.__init__(self) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) + + @staticmethod + def config_schema() -> vol.Schema: + """Return the config schema.""" + return DISCOVERY_SCHEMA + + def _setup_from_config(self, config: ConfigType) -> None: + """(Re)Setup the entity.""" + self._topic = {key: config.get(key) for key in (CONF_IMAGE_TOPIC,)} + self._attr_content_type = config[CONF_CONTENT_TYPE] + + def _prepare_subscribe_topics(self) -> None: + """(Re)Subscribe to topics.""" + + topics: dict[str, Any] = {} + + @callback + @log_messages(self.hass, self.entity_id) + def image_data_received(msg: ReceiveMessage) -> None: + """Handle new MQTT messages.""" + try: + if CONF_IMAGE_ENCODING in self._config: + self._last_image = b64decode(msg.payload) + else: + assert isinstance(msg.payload, bytes) + self._last_image = msg.payload + except (binascii.Error, ValueError, AssertionError) as err: + _LOGGER.error( + "Error processing image data received at topic %s: %s", + msg.topic, + err, + ) + self._last_image = None + self._attr_image_last_updated = dt_util.utcnow() + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + + topics[self._config[CONF_IMAGE_TOPIC]] = { + "topic": self._config[CONF_IMAGE_TOPIC], + "msg_callback": image_data_received, + "qos": self._config[CONF_QOS], + "encoding": None, + } + + self._sub_state = subscription.async_prepare_subscribe_topics( + self.hass, self._sub_state, topics + ) + + async def _subscribe_topics(self) -> None: + """(Re)Subscribe to topics.""" + await subscription.async_subscribe_topics(self.hass, self._sub_state) + + async def async_image(self) -> bytes | None: + """Return bytes of image.""" + return self._last_image diff --git a/tests/components/mqtt/test_image.py b/tests/components/mqtt/test_image.py new file mode 100644 index 00000000000..8cb7739cd7e --- /dev/null +++ b/tests/components/mqtt/test_image.py @@ -0,0 +1,521 @@ +"""The tests for mqtt image component.""" +from base64 import b64encode +from contextlib import suppress +from http import HTTPStatus +import json +from unittest.mock import patch + +import pytest +import respx + +from homeassistant.components import image, mqtt +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant + +from .test_common import ( + help_test_availability_when_connection_lost, + help_test_availability_without_topic, + help_test_custom_availability_payload, + help_test_default_availability_payload, + help_test_discovery_broken, + help_test_discovery_removal, + help_test_discovery_update, + help_test_discovery_update_attr, + help_test_discovery_update_unchanged, + help_test_entity_debug_info_message, + help_test_entity_device_info_remove, + help_test_entity_device_info_update, + help_test_entity_device_info_with_connection, + help_test_entity_device_info_with_identifier, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, + help_test_reloadable, + help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, + help_test_unique_id, + help_test_unload_config_entry_with_platform, + help_test_update_with_json_attrs_bad_json, + help_test_update_with_json_attrs_not_dict, +) + +from tests.common import async_fire_mqtt_message +from tests.typing import ( + ClientSessionGenerator, + MqttMockHAClientGenerator, + MqttMockPahoClient, +) + +DEFAULT_CONFIG = { + mqtt.DOMAIN: {image.DOMAIN: {"name": "test", "image_topic": "test_topic"}} +} + + +@pytest.fixture(autouse=True) +def image_platform_only(): + """Only setup the image platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.IMAGE]): + yield + + +@pytest.mark.freeze_time("2023-04-01 00:00:00+00:00") +@pytest.mark.parametrize( + "hass_config", + [{mqtt.DOMAIN: {image.DOMAIN: {"image_topic": "test/image", "name": "Test"}}}], +) +async def test_run_image_setup( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test that it fetches the given payload.""" + topic = "test/image" + await mqtt_mock_entry() + + state = hass.states.get("image.test") + assert state.state == STATE_UNKNOWN + access_token = state.attributes["access_token"] + assert state.attributes == { + "access_token": access_token, + "entity_picture": f"/api/image_proxy/image.test?token={access_token}", + "friendly_name": "Test", + } + + async_fire_mqtt_message(hass, topic, b"grass") + client = await hass_client_no_auth() + resp = await client.get(state.attributes["entity_picture"]) + assert resp.status == HTTPStatus.OK + body = await resp.read() + assert body == b"grass" + + state = hass.states.get("image.test") + assert state.state == "2023-04-01T00:00:00+00:00" + + +@pytest.mark.freeze_time("2023-04-01 00:00:00+00:00") +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + image.DOMAIN: { + "image_topic": "test/image", + "name": "Test", + "image_encoding": "b64", + "content_type": "image/png", + } + } + } + ], +) +async def test_run_image_b64_encoded( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that it fetches the given encoded payload.""" + topic = "test/image" + await mqtt_mock_entry() + + state = hass.states.get("image.test") + assert state.state == STATE_UNKNOWN + access_token = state.attributes["access_token"] + assert state.attributes == { + "access_token": access_token, + "entity_picture": f"/api/image_proxy/image.test?token={access_token}", + "friendly_name": "Test", + } + + # Fire incorrect encoded message (utf-8 encoded string) + async_fire_mqtt_message(hass, topic, "grass") + client = await hass_client_no_auth() + resp = await client.get(state.attributes["entity_picture"]) + assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR + assert "Error processing image data received at topic test/image" in caplog.text + + # Fire correctly encoded message (b64 encoded payload) + async_fire_mqtt_message(hass, topic, b64encode(b"grass")) + client = await hass_client_no_auth() + resp = await client.get(state.attributes["entity_picture"]) + assert resp.status == HTTPStatus.OK + body = await resp.read() + assert body == b"grass" + + state = hass.states.get("image.test") + assert state.state == "2023-04-01T00:00:00+00:00" + + +@pytest.mark.freeze_time("2023-04-01 00:00:00+00:00") +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + "image": { + "image_topic": "test/image", + "name": "Test", + "encoding": "utf-8", + "image_encoding": "b64", + "availability": {"topic": "test/image_availability"}, + } + } + } + ], +) +async def test_image_b64_encoded_with_availability( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test availability works if b64 encoding is turned on.""" + topic = "test/image" + topic_availability = "test/image_availability" + await mqtt_mock_entry() + + state = hass.states.get("image.test") + assert state is not None + assert state.state == STATE_UNAVAILABLE + + # Make sure we are available + async_fire_mqtt_message(hass, topic_availability, "online") + + state = hass.states.get("image.test") + assert state is not None + assert state.state == STATE_UNKNOWN + + url = hass.states.get("image.test").attributes["entity_picture"] + + async_fire_mqtt_message(hass, topic, b64encode(b"grass")) + + client = await hass_client_no_auth() + resp = await client.get(url) + assert resp.status == HTTPStatus.OK + body = await resp.text() + assert body == "grass" + + state = hass.states.get("image.test") + assert state.state == "2023-04-01T00:00:00+00:00" + + +@respx.mock +@pytest.mark.freeze_time("2023-04-01 00:00:00+00:00") +@pytest.mark.parametrize( + ("hass_config", "error_msg"), + [ + ( + { + mqtt.DOMAIN: { + "image": { + "name": "Test", + "encoding": "utf-8", + } + } + }, + "Invalid config for [mqtt]: required key not provided @ data['mqtt']['image'][0]['image_topic']. Got None.", + ), + ], +) +async def test_image_config_fails( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, + error_msg: str, +) -> None: + """Test setup with minimum configuration.""" + with suppress(AssertionError): + await mqtt_mock_entry() + assert error_msg in caplog.text + + +@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +async def test_availability_when_connection_lost( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability after MQTT disconnection.""" + await help_test_availability_when_connection_lost( + hass, mqtt_mock_entry, image.DOMAIN + ) + + +@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +async def test_availability_without_topic( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock_entry, image.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_payload( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability by default payload with defined topic.""" + await help_test_default_availability_payload( + hass, mqtt_mock_entry, image.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_custom_availability_payload( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test availability by custom payload with defined topic.""" + await help_test_custom_availability_payload( + hass, mqtt_mock_entry, image.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_setting_attribute_via_mqtt_json_message( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry, image.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_setting_attribute_with_template( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_with_template( + hass, mqtt_mock_entry, image.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_update_with_json_attrs_not_dict( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_not_dict( + hass, + mqtt_mock_entry, + caplog, + image.DOMAIN, + DEFAULT_CONFIG, + ) + + +async def test_update_with_json_attrs_bad_json( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_bad_json( + hass, + mqtt_mock_entry, + caplog, + image.DOMAIN, + DEFAULT_CONFIG, + ) + + +async def test_discovery_update_attr( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update of discovered MQTTAttributes.""" + await help_test_discovery_update_attr( + hass, + mqtt_mock_entry, + caplog, + image.DOMAIN, + DEFAULT_CONFIG, + ) + + +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + image.DOMAIN: [ + { + "name": "Test 1", + "image_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "name": "Test 2", + "image_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } + } + ], +) +async def test_unique_id( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test unique id option only creates one image per unique_id.""" + await help_test_unique_id(hass, mqtt_mock_entry, image.DOMAIN) + + +async def test_discovery_removal_image( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test removal of discovered image.""" + data = json.dumps(DEFAULT_CONFIG[mqtt.DOMAIN][image.DOMAIN]) + await help_test_discovery_removal(hass, mqtt_mock_entry, caplog, image.DOMAIN, data) + + +async def test_discovery_update_image( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update of discovered image.""" + config1 = {"name": "Beer", "image_topic": "test_topic"} + config2 = {"name": "Milk", "image_topic": "test_topic"} + + await help_test_discovery_update( + hass, mqtt_mock_entry, caplog, image.DOMAIN, config1, config2 + ) + + +async def test_discovery_update_unchanged_image( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update of discovered image.""" + data1 = '{ "name": "Beer", "image_topic": "test_topic"}' + with patch( + "homeassistant.components.mqtt.image.MqttImage.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, + mqtt_mock_entry, + caplog, + image.DOMAIN, + data1, + discovery_update, + ) + + +@pytest.mark.no_fail_on_log_exception +async def test_discovery_broken( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling of bad discovery message.""" + data1 = '{ "name": "Beer" }' + data2 = '{ "name": "Milk", "image_topic": "test_topic"}' + + await help_test_discovery_broken( + hass, mqtt_mock_entry, caplog, image.DOMAIN, data1, data2 + ) + + +async def test_entity_device_info_with_connection( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT image device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock_entry, image.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_with_identifier( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT image device registry integration.""" + await help_test_entity_device_info_with_identifier( + hass, mqtt_mock_entry, image.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_update( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test device registry update.""" + await help_test_entity_device_info_update( + hass, mqtt_mock_entry, image.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_remove( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test device registry remove.""" + await help_test_entity_device_info_remove( + hass, mqtt_mock_entry, image.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_id_update_subscriptions( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT subscriptions are managed when entity_id is updated.""" + await help_test_entity_id_update_subscriptions( + hass, + mqtt_mock_entry, + image.DOMAIN, + DEFAULT_CONFIG, + ["test_topic"], + ) + + +async def test_entity_id_update_discovery_update( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT discovery update when entity_id is updated.""" + await help_test_entity_id_update_discovery_update( + hass, mqtt_mock_entry, image.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_message( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, + mqtt_mock_entry, + image.DOMAIN, + DEFAULT_CONFIG, + None, + state_topic="test_topic", + state_payload=b"ON", + ) + + +async def test_reloadable( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, +) -> None: + """Test reloading the MQTT platform.""" + domain = image.DOMAIN + config = DEFAULT_CONFIG + await help_test_reloadable(hass, mqtt_client_mock, domain, config) + + +@pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) +async def test_setup_manual_entity_from_yaml( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test setup manual configured MQTT entity.""" + await mqtt_mock_entry() + platform = image.DOMAIN + assert hass.states.get(f"{platform}.test") + + +async def test_unload_entry( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test unloading the config entry.""" + domain = image.DOMAIN + config = DEFAULT_CONFIG + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry, domain, config + )