diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 2e50207fe53..1180e0a01d2 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -145,7 +145,7 @@ async def async_install_addon(hass: HomeAssistantType, slug: str) -> dict: """ hassio = hass.data[DOMAIN] command = f"/addons/{slug}/install" - return await hassio.send_command(command) + return await hassio.send_command(command, timeout=None) @bind_hass @@ -169,7 +169,7 @@ async def async_start_addon(hass: HomeAssistantType, slug: str) -> dict: """ hassio = hass.data[DOMAIN] command = f"/addons/{slug}/start" - return await hassio.send_command(command) + return await hassio.send_command(command, timeout=60) @bind_hass diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index f57d737bf36..5706b75efb8 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -20,6 +20,7 @@ from openzwavemqtt.models.value import OZWValue import voluptuous as vol from homeassistant.components import mqtt +from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg @@ -27,6 +28,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from . import const from .const import ( + CONF_INTEGRATION_CREATED_ADDON, DATA_UNSUBSCRIBE, DOMAIN, MANAGER, @@ -265,6 +267,22 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return True +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Remove a config entry.""" + if not entry.data.get(CONF_INTEGRATION_CREATED_ADDON): + return + + try: + await hass.components.hassio.async_stop_addon("core_zwave") + except HassioAPIError as err: + _LOGGER.error("Failed to stop the OpenZWave add-on: %s", err) + return + try: + await hass.components.hassio.async_uninstall_addon("core_zwave") + except HassioAPIError as err: + _LOGGER.error("Failed to uninstall the OpenZWave add-on: %s", err) + + async def async_handle_remove_node(hass: HomeAssistant, node: OZWNode): """Handle the removal of a Z-Wave node, removing all traces in device/entity registry.""" dev_registry = await get_dev_reg(hass) diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py index 3153324322e..1c0ccdefa70 100644 --- a/homeassistant/components/ozw/config_flow.py +++ b/homeassistant/components/ozw/config_flow.py @@ -1,10 +1,26 @@ """Config flow for ozw integration.""" -from homeassistant import config_entries +import logging +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import callback +from homeassistant.data_entry_flow import AbortFlow + +from .const import CONF_INTEGRATION_CREATED_ADDON from .const import DOMAIN # pylint:disable=unused-import +_LOGGER = logging.getLogger(__name__) + +CONF_ADDON_DEVICE = "device" +CONF_ADDON_NETWORK_KEY = "network_key" +CONF_NETWORK_KEY = "network_key" +CONF_USB_PATH = "usb_path" +CONF_USE_ADDON = "use_addon" TITLE = "OpenZWave" +ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=False): bool}) + class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for ozw.""" @@ -12,13 +28,156 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + def __init__(self): + """Set up flow instance.""" + self.addon_config = None + self.network_key = None + self.usb_path = None + self.use_addon = False + # If we install the add-on we should uninstall it on entry remove. + self.integration_created_addon = False + async def async_step_user(self, user_input=None): """Handle the initial step.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") + + # Currently all flow results need the MQTT integration. + # This will change when we have the direct MQTT client connection. + # When that is implemented, move this check to _async_use_mqtt_integration. if "mqtt" not in self.hass.config.components: return self.async_abort(reason="mqtt_required") - if user_input is not None: - return self.async_create_entry(title=TITLE, data={}) - return self.async_show_form(step_id="user") + if not self.hass.components.hassio.is_hassio(): + return self._async_use_mqtt_integration() + + return await self.async_step_on_supervisor() + + def _async_create_entry_from_vars(self): + """Return a config entry for the flow.""" + return self.async_create_entry( + title=TITLE, + data={ + CONF_USB_PATH: self.usb_path, + CONF_NETWORK_KEY: self.network_key, + CONF_USE_ADDON: self.use_addon, + CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addon, + }, + ) + + @callback + def _async_use_mqtt_integration(self): + """Handle logic when using the MQTT integration. + + This is the entry point for the logic that is needed + when this integration will depend on the MQTT integration. + """ + return self._async_create_entry_from_vars() + + async def async_step_on_supervisor(self, user_input=None): + """Handle logic when on Supervisor host.""" + if user_input is None: + return self.async_show_form( + step_id="on_supervisor", data_schema=ON_SUPERVISOR_SCHEMA + ) + if not user_input[CONF_USE_ADDON]: + return self._async_create_entry_from_vars() + + self.use_addon = True + + if await self._async_is_addon_running(): + return self._async_create_entry_from_vars() + + if await self._async_is_addon_installed(): + return await self.async_step_start_addon() + + return await self.async_step_install_addon() + + async def async_step_install_addon(self): + """Install OpenZWave add-on.""" + try: + await self.hass.components.hassio.async_install_addon("core_zwave") + except self.hass.components.hassio.HassioAPIError as err: + _LOGGER.error("Failed to install OpenZWave add-on: %s", err) + return self.async_abort(reason="addon_install_failed") + self.integration_created_addon = True + + return await self.async_step_start_addon() + + async def async_step_start_addon(self, user_input=None): + """Ask for config and start OpenZWave add-on.""" + if self.addon_config is None: + self.addon_config = await self._async_get_addon_config() + + errors = {} + + if user_input is not None: + self.network_key = user_input[CONF_NETWORK_KEY] + self.usb_path = user_input[CONF_USB_PATH] + + new_addon_config = {CONF_ADDON_DEVICE: self.usb_path} + if self.network_key: + new_addon_config[CONF_ADDON_NETWORK_KEY] = self.network_key + + if new_addon_config != self.addon_config: + await self._async_set_addon_config(new_addon_config) + + try: + await self.hass.components.hassio.async_start_addon("core_zwave") + except self.hass.components.hassio.HassioAPIError as err: + _LOGGER.error("Failed to start OpenZWave add-on: %s", err) + errors["base"] = "addon_start_failed" + else: + return self._async_create_entry_from_vars() + + self.usb_path = self.addon_config.get(CONF_ADDON_DEVICE, "") + self.network_key = self.addon_config.get(CONF_ADDON_NETWORK_KEY, "") + + data_schema = vol.Schema( + { + vol.Required(CONF_USB_PATH, default=self.usb_path): str, + vol.Optional(CONF_NETWORK_KEY, default=self.network_key): str, + } + ) + + return self.async_show_form( + step_id="start_addon", data_schema=data_schema, errors=errors + ) + + async def _async_get_addon_info(self): + """Return and cache OpenZWave add-on info.""" + try: + addon_info = await self.hass.components.hassio.async_get_addon_info( + "core_zwave" + ) + except self.hass.components.hassio.HassioAPIError as err: + _LOGGER.error("Failed to get OpenZWave add-on info: %s", err) + raise AbortFlow("addon_info_failed") from err + + return addon_info + + async def _async_is_addon_running(self): + """Return True if OpenZWave add-on is running.""" + addon_info = await self._async_get_addon_info() + return addon_info["state"] == "started" + + async def _async_is_addon_installed(self): + """Return True if OpenZWave add-on is installed.""" + addon_info = await self._async_get_addon_info() + return addon_info["version"] is not None + + async def _async_get_addon_config(self): + """Get OpenZWave add-on config.""" + addon_info = await self._async_get_addon_info() + return addon_info["options"] + + async def _async_set_addon_config(self, config): + """Set OpenZWave add-on config.""" + options = {"options": config} + try: + await self.hass.components.hassio.async_set_addon_options( + "core_zwave", options + ) + except self.hass.components.hassio.HassioAPIError as err: + _LOGGER.error("Failed to set OpenZWave add-on config: %s", err) + raise AbortFlow("addon_set_config_failed") from err diff --git a/homeassistant/components/ozw/const.py b/homeassistant/components/ozw/const.py index 8e5007a8419..160b251eeca 100644 --- a/homeassistant/components/ozw/const.py +++ b/homeassistant/components/ozw/const.py @@ -10,6 +10,9 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN DOMAIN = "ozw" DATA_UNSUBSCRIBE = "unsubscribe" + +CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon" + PLATFORMS = [ BINARY_SENSOR_DOMAIN, COVER_DOMAIN, diff --git a/homeassistant/components/ozw/strings.json b/homeassistant/components/ozw/strings.json index 88f8911db0d..52317b3d6a8 100644 --- a/homeassistant/components/ozw/strings.json +++ b/homeassistant/components/ozw/strings.json @@ -1,13 +1,25 @@ { "config": { "step": { - "user": { - "title": "Confirm set up" + "on_supervisor": { + "title": "Select connection method", + "description": "Do you want to use the OpenZWave Supervisor add-on?", + "data": {"use_addon": "Use the OpenZWave Supervisor add-on"} + }, + "start_addon": { + "title": "Enter the OpenZWave add-on configuration", + "data": {"usb_path": "[%key:common::config_flow::data::usb_path%]", "network_key": "Network Key"} } }, "abort": { + "addon_info_failed": "Failed to get OpenZWave add-on info.", + "addon_install_failed": "Failed to install the OpenZWave add-on.", + "addon_set_config_failed": "Failed to set OpenZWave configuration.", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "mqtt_required": "The MQTT integration is not set up" + }, + "error": { + "addon_start_failed": "Failed to start the OpenZWave add-on. Check the configuration." } } } diff --git a/homeassistant/components/ozw/translations/en.json b/homeassistant/components/ozw/translations/en.json index 4e41ee58d11..e028e1923ae 100644 --- a/homeassistant/components/ozw/translations/en.json +++ b/homeassistant/components/ozw/translations/en.json @@ -1,13 +1,29 @@ { "config": { "abort": { + "addon_info_failed": "Failed to get OpenZWave add-on info.", + "addon_install_failed": "Failed to install the OpenZWave add-on.", + "addon_set_config_failed": "Failed to set OpenZWave configuration.", "mqtt_required": "The MQTT integration is not set up", - "one_instance_allowed": "The integration only supports one Z-Wave instance", "single_instance_allowed": "Already configured. Only a single configuration possible." }, + "error": { + "addon_start_failed": "Failed to start the OpenZWave add-on. Check the configuration." + }, "step": { - "user": { - "title": "Confirm set up" + "on_supervisor": { + "data": { + "use_addon": "Use the OpenZWave Supervisor add-on" + }, + "description": "Do you want to use the OpenZWave Supervisor add-on?", + "title": "Select connection method" + }, + "start_addon": { + "data": { + "network_key": "Network Key", + "usb_path": "USB Device Path" + }, + "title": "Enter the OpenZWave add-on configuration" } } } diff --git a/tests/components/ozw/conftest.py b/tests/components/ozw/conftest.py index e09dacd8342..f2518fb8007 100644 --- a/tests/components/ozw/conftest.py +++ b/tests/components/ozw/conftest.py @@ -237,3 +237,19 @@ async def lock_msg_fixture(hass): message = MQTTMessage(topic=lock_json["topic"], payload=lock_json["payload"]) message.encode() return message + + +@pytest.fixture(name="stop_addon") +def mock_install_addon(): + """Mock stop add-on.""" + with patch("homeassistant.components.hassio.async_stop_addon") as stop_addon: + yield stop_addon + + +@pytest.fixture(name="uninstall_addon") +def mock_uninstall_addon(): + """Mock uninstall add-on.""" + with patch( + "homeassistant.components.hassio.async_uninstall_addon" + ) as uninstall_addon: + yield uninstall_addon diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py index f6b9f388146..7561244999d 100644 --- a/tests/components/ozw/test_config_flow.py +++ b/tests/components/ozw/test_config_flow.py @@ -1,5 +1,8 @@ """Test the Z-Wave over MQTT config flow.""" +import pytest + from homeassistant import config_entries, setup +from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.ozw.config_flow import TITLE from homeassistant.components.ozw.const import DOMAIN @@ -7,15 +10,70 @@ from tests.async_mock import patch from tests.common import MockConfigEntry -async def test_user_create_entry(hass): - """Test the user step creates an entry.""" +@pytest.fixture(name="supervisor") +def mock_supervisor_fixture(): + """Mock Supervisor.""" + with patch("homeassistant.components.hassio.is_hassio", return_value=True): + yield + + +@pytest.fixture(name="addon_info") +def mock_addon_info(): + """Mock Supervisor add-on info.""" + with patch("homeassistant.components.hassio.async_get_addon_info") as addon_info: + addon_info.return_value = {} + yield addon_info + + +@pytest.fixture(name="addon_running") +def mock_addon_running(addon_info): + """Mock add-on already running.""" + addon_info.return_value["state"] = "started" + return addon_info + + +@pytest.fixture(name="addon_installed") +def mock_addon_installed(addon_info): + """Mock add-on already installed but not running.""" + addon_info.return_value["state"] = "stopped" + addon_info.return_value["version"] = "1.0" + return addon_info + + +@pytest.fixture(name="addon_options") +def mock_addon_options(addon_info): + """Mock add-on options.""" + addon_info.return_value["options"] = {} + return addon_info + + +@pytest.fixture(name="set_addon_options") +def mock_set_addon_options(): + """Mock set add-on options.""" + with patch( + "homeassistant.components.hassio.async_set_addon_options" + ) as set_options: + yield set_options + + +@pytest.fixture(name="install_addon") +def mock_install_addon(): + """Mock install add-on.""" + with patch("homeassistant.components.hassio.async_install_addon") as install_addon: + yield install_addon + + +@pytest.fixture(name="start_addon") +def mock_start_addon(): + """Mock start add-on.""" + with patch("homeassistant.components.hassio.async_start_addon") as start_addon: + yield start_addon + + +async def test_user_not_supervisor_create_entry(hass): + """Test the user step creates an entry not on Supervisor.""" hass.config.components.add("mqtt") await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "form" - assert result["errors"] is None with patch( "homeassistant.components.ozw.async_setup", return_value=True @@ -23,12 +81,19 @@ async def test_user_create_entry(hass): "homeassistant.components.ozw.async_setup_entry", return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" - assert result2["title"] == TITLE - assert result2["data"] == {} + assert result["type"] == "create_entry" + assert result["title"] == TITLE + assert result["data"] == { + "usb_path": None, + "network_key": None, + "use_addon": False, + "integration_created_addon": False, + } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -52,3 +117,230 @@ async def test_one_instance_allowed(hass): ) assert result["type"] == "abort" assert result["reason"] == "single_instance_allowed" + + +async def test_not_addon(hass, supervisor): + """Test opting out of add-on on Supervisor.""" + hass.config.components.add("mqtt") + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ozw.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.ozw.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": False} + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == TITLE + assert result["data"] == { + "usb_path": None, + "network_key": None, + "use_addon": False, + "integration_created_addon": False, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_addon_running(hass, supervisor, addon_running): + """Test add-on already running on Supervisor.""" + hass.config.components.add("mqtt") + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ozw.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.ozw.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == TITLE + assert result["data"] == { + "usb_path": None, + "network_key": None, + "use_addon": True, + "integration_created_addon": False, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_addon_info_failure(hass, supervisor, addon_info): + """Test add-on info failure.""" + hass.config.components.add("mqtt") + addon_info.side_effect = HassioAPIError() + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert result["type"] == "abort" + assert result["reason"] == "addon_info_failed" + + +async def test_addon_installed( + hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon +): + """Test add-on already installed but not running on Supervisor.""" + hass.config.components.add("mqtt") + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + with patch( + "homeassistant.components.ozw.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.ozw.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == TITLE + assert result["data"] == { + "usb_path": "/test", + "network_key": "abc123", + "use_addon": True, + "integration_created_addon": False, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_set_addon_config_failure( + hass, supervisor, addon_installed, addon_options, set_addon_options +): + """Test add-on set config failure.""" + hass.config.components.add("mqtt") + set_addon_options.side_effect = HassioAPIError() + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} + ) + + assert result["type"] == "abort" + assert result["reason"] == "addon_set_config_failed" + + +async def test_start_addon_failure( + hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon +): + """Test add-on start failure.""" + hass.config.components.add("mqtt") + start_addon.side_effect = HassioAPIError() + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} + ) + + assert result["type"] == "form" + assert result["errors"] == {"base": "addon_start_failed"} + + +async def test_addon_not_installed( + hass, + supervisor, + addon_installed, + install_addon, + addon_options, + set_addon_options, + start_addon, +): + """Test add-on not installed.""" + hass.config.components.add("mqtt") + addon_installed.return_value["version"] = None + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + with patch( + "homeassistant.components.ozw.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.ozw.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == TITLE + assert result["data"] == { + "usb_path": "/test", + "network_key": "abc123", + "use_addon": True, + "integration_created_addon": True, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_install_addon_failure(hass, supervisor, addon_installed, install_addon): + """Test add-on install failure.""" + hass.config.components.add("mqtt") + addon_installed.return_value["version"] = None + install_addon.side_effect = HassioAPIError() + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert result["type"] == "abort" + assert result["reason"] == "addon_install_failed" diff --git a/tests/components/ozw/test_init.py b/tests/components/ozw/test_init.py index 9a76b4906fa..bf1eefe866a 100644 --- a/tests/components/ozw/test_init.py +++ b/tests/components/ozw/test_init.py @@ -1,5 +1,6 @@ """Test integration initialization.""" from homeassistant import config_entries +from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.ozw import DOMAIN, PLATFORMS, const from .common import setup_ozw @@ -60,3 +61,70 @@ async def test_unload_entry(hass, generic_data, switch_msg, caplog): assert len(hass.states.async_entity_ids("switch")) == 1 for record in caplog.records: assert record.levelname != "ERROR" + + +async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): + """Test remove the config entry.""" + # test successful remove without created add-on + entry = MockConfigEntry( + domain=DOMAIN, + title="Z-Wave", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + data={"integration_created_addon": False}, + ) + entry.add_to_hass(hass) + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + await hass.config_entries.async_remove(entry.entry_id) + + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + + # test successful remove with created add-on + entry = MockConfigEntry( + domain=DOMAIN, + title="Z-Wave", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + data={"integration_created_addon": True}, + ) + entry.add_to_hass(hass) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + await hass.config_entries.async_remove(entry.entry_id) + + stop_addon.call_count == 1 + uninstall_addon.call_count == 1 + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + stop_addon.reset_mock() + uninstall_addon.reset_mock() + + # test add-on stop failure + entry.add_to_hass(hass) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + stop_addon.side_effect = HassioAPIError() + + await hass.config_entries.async_remove(entry.entry_id) + + stop_addon.call_count == 1 + uninstall_addon.call_count == 0 + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + assert "Failed to stop the OpenZWave add-on" in caplog.text + stop_addon.side_effect = None + stop_addon.reset_mock() + uninstall_addon.reset_mock() + + # test add-on uninstall failure + entry.add_to_hass(hass) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + uninstall_addon.side_effect = HassioAPIError() + + await hass.config_entries.async_remove(entry.entry_id) + + stop_addon.call_count == 1 + uninstall_addon.call_count == 1 + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + assert "Failed to uninstall the OpenZWave add-on" in caplog.text