From 9043b7b214256f82ec526176ed38baca1a3ada52 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 2 Dec 2020 20:03:29 +0100 Subject: [PATCH] Add ozw add-on discovery and mqtt client (#43838) --- homeassistant/components/hassio/__init__.py | 13 ++ homeassistant/components/ozw/__init__.py | 82 +++++++++-- homeassistant/components/ozw/config_flow.py | 34 ++++- homeassistant/components/ozw/const.py | 2 +- homeassistant/components/ozw/manifest.json | 4 +- homeassistant/components/ozw/strings.json | 12 +- .../components/ozw/translations/en.json | 5 + homeassistant/components/ozw/websocket_api.py | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ozw/conftest.py | 9 ++ tests/components/ozw/test_config_flow.py | 129 +++++++++++++++++- tests/components/ozw/test_init.py | 85 ++++++++++++ 13 files changed, 353 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 1e05321b60f..e8b874b2334 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging import os +from typing import Optional import voluptuous as vol @@ -23,6 +24,7 @@ from homeassistant.util.dt import utcnow from .addon_panel import async_setup_addon_panel from .auth import async_setup_auth_view +from .const import ATTR_DISCOVERY from .discovery import async_setup_discovery_view from .handler import HassIO, HassioAPIError, api_data from .http import HassIOView @@ -200,6 +202,17 @@ async def async_set_addon_options( return await hassio.send_command(command, payload=options) +@bind_hass +async def async_get_addon_discovery_info( + hass: HomeAssistantType, slug: str +) -> Optional[dict]: + """Return discovery data for an add-on.""" + hassio = hass.data[DOMAIN] + data = await hassio.retrieve_discovery_messages() + discovered_addons = data[ATTR_DISCOVERY] + return next((addon for addon in discovered_addons if addon["addon"] == slug), None) + + @callback @bind_hass def get_info(hass): diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index 2fe45018182..c0d50e18abc 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -17,22 +17,25 @@ from openzwavemqtt.const import ( ) from openzwavemqtt.models.node import OZWNode from openzwavemqtt.models.value import OZWValue +from openzwavemqtt.util.mqtt_client import MQTTClient import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg from homeassistant.helpers.dispatcher import async_dispatcher_send from . import const from .const import ( CONF_INTEGRATION_CREATED_ADDON, + CONF_USE_ADDON, DATA_UNSUBSCRIBE, DOMAIN, MANAGER, - OPTIONS, PLATFORMS, TOPIC_OPENZWAVE, ) @@ -50,13 +53,11 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) DATA_DEVICES = "zwave-mqtt-devices" +DATA_STOP_MQTT_CLIENT = "ozw_stop_mqtt_client" async def async_setup(hass: HomeAssistant, config: dict): """Initialize basic config of ozw component.""" - if "mqtt" not in hass.config.components: - _LOGGER.error("MQTT integration is not set up") - return False hass.data[DOMAIN] = {} return True @@ -69,16 +70,46 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): data_nodes = {} data_values = {} removed_nodes = [] + manager_options = {"topic_prefix": f"{TOPIC_OPENZWAVE}/"} - @callback - def send_message(topic, payload): - mqtt.async_publish(hass, topic, json.dumps(payload)) + if entry.unique_id is None: + hass.config_entries.async_update_entry(entry, unique_id=DOMAIN) - options = OZWOptions(send_message=send_message, topic_prefix=f"{TOPIC_OPENZWAVE}/") + if entry.data.get(CONF_USE_ADDON): + # Do not use MQTT integration. Use own MQTT client. + # Retrieve discovery info from the OpenZWave add-on. + discovery_info = await hass.components.hassio.async_get_addon_discovery_info( + "core_zwave" + ) + + if not discovery_info: + _LOGGER.error("Failed to get add-on discovery info") + raise ConfigEntryNotReady + + discovery_info_config = discovery_info["config"] + + host = discovery_info_config["host"] + port = discovery_info_config["port"] + username = discovery_info_config["username"] + password = discovery_info_config["password"] + mqtt_client = MQTTClient(host, port, username=username, password=password) + manager_options["send_message"] = mqtt_client.send_message + + else: + if "mqtt" not in hass.config.components: + _LOGGER.error("MQTT integration is not set up") + return False + + @callback + def send_message(topic, payload): + mqtt.async_publish(hass, topic, json.dumps(payload)) + + manager_options["send_message"] = send_message + + options = OZWOptions(**manager_options) manager = OZWManager(options) hass.data[DOMAIN][MANAGER] = manager - hass.data[DOMAIN][OPTIONS] = options @callback def async_node_added(node): @@ -234,11 +265,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): for component in PLATFORMS ] ) - ozw_data[DATA_UNSUBSCRIBE].append( - await mqtt.async_subscribe( - hass, f"{TOPIC_OPENZWAVE}/#", async_receive_message + if entry.data.get(CONF_USE_ADDON): + mqtt_client_task = asyncio.create_task(mqtt_client.start_client(manager)) + + async def async_stop_mqtt_client(event=None): + """Stop the mqtt client. + + Do not unsubscribe the manager topic. + """ + mqtt_client_task.cancel() + try: + await mqtt_client_task + except asyncio.CancelledError: + pass + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt_client) + ozw_data[DATA_STOP_MQTT_CLIENT] = async_stop_mqtt_client + + else: + ozw_data[DATA_UNSUBSCRIBE].append( + await mqtt.async_subscribe( + hass, f"{manager.options.topic_prefix}#", async_receive_message + ) ) - ) hass.async_create_task(start_platforms()) @@ -262,6 +311,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): # unsubscribe all listeners for unsubscribe_listener in hass.data[DOMAIN][entry.entry_id][DATA_UNSUBSCRIBE]: unsubscribe_listener() + + if entry.data.get(CONF_USE_ADDON): + async_stop_mqtt_client = hass.data[DOMAIN][entry.entry_id][ + DATA_STOP_MQTT_CLIENT + ] + await async_stop_mqtt_client() + hass.data[DOMAIN].pop(entry.entry_id) return True diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py index 9ecbcbe76bb..4543bc27984 100644 --- a/homeassistant/components/ozw/config_flow.py +++ b/homeassistant/components/ozw/config_flow.py @@ -7,7 +7,7 @@ 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 CONF_INTEGRATION_CREATED_ADDON, CONF_USE_ADDON from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -16,7 +16,6 @@ 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}) @@ -43,17 +42,36 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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") + # Set a unique_id to make sure discovery flow is aborted on progress. + await self.async_set_unique_id(DOMAIN, raise_on_progress=False) if not self.hass.components.hassio.is_hassio(): return self._async_use_mqtt_integration() return await self.async_step_on_supervisor() + async def async_step_hassio(self, discovery_info): + """Receive configuration from add-on discovery info. + + This flow is triggered by the OpenZWave add-on. + """ + await self.async_set_unique_id(DOMAIN) + self._abort_if_unique_id_configured() + + addon_config = await self._async_get_addon_config() + self.usb_path = addon_config[CONF_ADDON_DEVICE] + self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "") + + return await self.async_step_hassio_confirm() + + async def async_step_hassio_confirm(self, user_input=None): + """Confirm the add-on discovery.""" + if user_input is not None: + self.use_addon = True + return self._async_create_entry_from_vars() + + return self.async_show_form(step_id="hassio_confirm") + def _async_create_entry_from_vars(self): """Return a config entry for the flow.""" return self.async_create_entry( @@ -73,6 +91,8 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): This is the entry point for the logic that is needed when this integration will depend on the MQTT integration. """ + if "mqtt" not in self.hass.config.components: + return self.async_abort(reason="mqtt_required") return self._async_create_entry_from_vars() async def async_step_on_supervisor(self, user_input=None): diff --git a/homeassistant/components/ozw/const.py b/homeassistant/components/ozw/const.py index 160b251eeca..f8d5090aa84 100644 --- a/homeassistant/components/ozw/const.py +++ b/homeassistant/components/ozw/const.py @@ -12,6 +12,7 @@ DOMAIN = "ozw" DATA_UNSUBSCRIBE = "unsubscribe" CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon" +CONF_USE_ADDON = "use_addon" PLATFORMS = [ BINARY_SENSOR_DOMAIN, @@ -24,7 +25,6 @@ PLATFORMS = [ SWITCH_DOMAIN, ] MANAGER = "manager" -OPTIONS = "options" # MQTT Topics TOPIC_OPENZWAVE = "OpenZWave" diff --git a/homeassistant/components/ozw/manifest.json b/homeassistant/components/ozw/manifest.json index fa25f984076..a1409fd79a8 100644 --- a/homeassistant/components/ozw/manifest.json +++ b/homeassistant/components/ozw/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ozw", "requirements": [ - "python-openzwave-mqtt==1.3.2" + "python-openzwave-mqtt[mqtt-client]==1.4.0" ], "after_dependencies": [ "mqtt" @@ -14,4 +14,4 @@ "@marcelveldt", "@MartinHjelmare" ] -} \ No newline at end of file +} diff --git a/homeassistant/components/ozw/strings.json b/homeassistant/components/ozw/strings.json index f006f0663f0..ed9816c57f2 100644 --- a/homeassistant/components/ozw/strings.json +++ b/homeassistant/components/ozw/strings.json @@ -4,17 +4,25 @@ "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"} + "data": { "use_addon": "Use the OpenZWave Supervisor add-on" } }, "install_addon": { "title": "The OpenZWave add-on installation has started" }, "start_addon": { "title": "Enter the OpenZWave add-on configuration", - "data": {"usb_path": "[%key:common::config_flow::data::usb_path%]", "network_key": "Network Key"} + "data": { + "usb_path": "[%key:common::config_flow::data::usb_path%]", + "network_key": "Network Key" + } + }, + "hassio_confirm": { + "title": "Set up OpenZWave integration with the OpenZWave add-on" } }, "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "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.", diff --git a/homeassistant/components/ozw/translations/en.json b/homeassistant/components/ozw/translations/en.json index e21b1819883..1c837e0bde5 100644 --- a/homeassistant/components/ozw/translations/en.json +++ b/homeassistant/components/ozw/translations/en.json @@ -4,6 +4,8 @@ "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.", + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", "mqtt_required": "The MQTT integration is not set up", "single_instance_allowed": "Already configured. Only a single configuration possible." }, @@ -14,6 +16,9 @@ "install_addon": "Please wait while the OpenZWave add-on installation finishes. This can take several minutes." }, "step": { + "hassio_confirm": { + "title": "Set up OpenZWave integration with the OpenZWave add-on" + }, "install_addon": { "title": "The OpenZWave add-on installation has started" }, diff --git a/homeassistant/components/ozw/websocket_api.py b/homeassistant/components/ozw/websocket_api.py index ea6df900907..3ee6e040743 100644 --- a/homeassistant/components/ozw/websocket_api.py +++ b/homeassistant/components/ozw/websocket_api.py @@ -21,7 +21,7 @@ from homeassistant.components import websocket_api from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from .const import ATTR_CONFIG_PARAMETER, ATTR_CONFIG_VALUE, DOMAIN, MANAGER, OPTIONS +from .const import ATTR_CONFIG_PARAMETER, ATTR_CONFIG_VALUE, DOMAIN, MANAGER from .lock import ATTR_USERCODE TYPE = "type" @@ -461,7 +461,7 @@ def websocket_refresh_node_info(hass, connection, msg): """Tell OpenZWave to re-interview a node.""" manager = hass.data[DOMAIN][MANAGER] - options = hass.data[DOMAIN][OPTIONS] + options = manager.options @callback def forward_node(node): diff --git a/requirements_all.txt b/requirements_all.txt index 92048522278..0ec2442425e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1794,7 +1794,7 @@ python-nest==4.1.0 python-nmap==0.6.1 # homeassistant.components.ozw -python-openzwave-mqtt==1.3.2 +python-openzwave-mqtt[mqtt-client]==1.4.0 # homeassistant.components.qbittorrent python-qbittorrent==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b274cc2df35..09a722d716d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -884,7 +884,7 @@ python-miio==0.5.4 python-nest==4.1.0 # homeassistant.components.ozw -python-openzwave-mqtt==1.3.2 +python-openzwave-mqtt[mqtt-client]==1.4.0 # homeassistant.components.songpal python-songpal==0.12 diff --git a/tests/components/ozw/conftest.py b/tests/components/ozw/conftest.py index f2518fb8007..d3f8288658c 100644 --- a/tests/components/ozw/conftest.py +++ b/tests/components/ozw/conftest.py @@ -253,3 +253,12 @@ def mock_uninstall_addon(): "homeassistant.components.hassio.async_uninstall_addon" ) as uninstall_addon: yield uninstall_addon + + +@pytest.fixture(name="get_addon_discovery_info") +def mock_get_addon_discovery_info(): + """Mock get add-on discovery info.""" + with patch( + "homeassistant.components.hassio.async_get_addon_discovery_info" + ) as get_addon_discovery_info: + yield get_addon_discovery_info diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py index 7a2b89967c5..289b6c7f4cd 100644 --- a/tests/components/ozw/test_config_flow.py +++ b/tests/components/ozw/test_config_flow.py @@ -9,6 +9,14 @@ from homeassistant.components.ozw.const import DOMAIN from tests.async_mock import patch from tests.common import MockConfigEntry +ADDON_DISCOVERY_INFO = { + "addon": "OpenZWave", + "host": "host1", + "port": 1234, + "username": "name1", + "password": "pass1", +} + @pytest.fixture(name="supervisor") def mock_supervisor_fixture(): @@ -44,7 +52,7 @@ def mock_addon_installed(addon_info): def mock_addon_options(addon_info): """Mock add-on options.""" addon_info.return_value["options"] = {} - return addon_info + return addon_info.return_value["options"] @pytest.fixture(name="set_addon_options") @@ -361,3 +369,122 @@ async def test_install_addon_failure(hass, supervisor, addon_installed, install_ assert result["type"] == "abort" assert result["reason"] == "addon_install_failed" + + +async def test_supervisor_discovery(hass, supervisor, addon_running, addon_options): + """Test flow started from Supervisor discovery.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + addon_options["device"] = "/test" + addon_options["network_key"] = "abc123" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + 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"], {}) + 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_clean_discovery_on_user_create( + hass, supervisor, addon_running, addon_options +): + """Test discovery flow is cleaned up when a user flow is finished.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + addon_options["device"] = "/test" + addon_options["network_key"] = "abc123" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + assert result["type"] == "form" + + 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 len(hass.config_entries.flow.async_progress()) == 0 + 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_abort_discovery_with_user_flow( + hass, supervisor, addon_running, addon_options +): + """Test discovery flow is aborted if a user flow is in progress.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_in_progress" + assert len(hass.config_entries.flow.async_progress()) == 1 + + +async def test_abort_discovery_with_existing_entry( + hass, supervisor, addon_running, addon_options +): + """Test discovery flow is aborted if an entry already exists.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry(domain=DOMAIN, data={}, title=TITLE, unique_id=DOMAIN) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" diff --git a/tests/components/ozw/test_init.py b/tests/components/ozw/test_init.py index bf1eefe866a..efc38fa63c2 100644 --- a/tests/components/ozw/test_init.py +++ b/tests/components/ozw/test_init.py @@ -5,6 +5,7 @@ from homeassistant.components.ozw import DOMAIN, PLATFORMS, const from .common import setup_ozw +from tests.async_mock import patch from tests.common import MockConfigEntry @@ -23,6 +24,18 @@ async def test_init_entry(hass, generic_data): assert hass.services.has_service(DOMAIN, const.SERVICE_REMOVE_NODE) +async def test_setup_entry_without_mqtt(hass): + """Test setting up config entry without mqtt integration setup.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="OpenZWave", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + ) + entry.add_to_hass(hass) + + assert not await hass.config_entries.async_setup(entry.entry_id) + + async def test_unload_entry(hass, generic_data, switch_msg, caplog): """Test unload the config entry.""" entry = MockConfigEntry( @@ -128,3 +141,75 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): 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 + + +async def test_setup_entry_with_addon(hass, get_addon_discovery_info): + """Test set up entry using OpenZWave add-on.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="OpenZWave", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + data={"use_addon": True}, + ) + entry.add_to_hass(hass) + + with patch("homeassistant.components.ozw.MQTTClient", autospec=True) as mock_client: + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert mock_client.return_value.start_client.call_count == 1 + + # Verify integration + platform loaded. + assert "ozw" in hass.config.components + for platform in PLATFORMS: + assert platform in hass.config.components, platform + assert f"{platform}.{DOMAIN}" in hass.config.components, f"{platform}.{DOMAIN}" + + # Verify services registered + assert hass.services.has_service(DOMAIN, const.SERVICE_ADD_NODE) + assert hass.services.has_service(DOMAIN, const.SERVICE_REMOVE_NODE) + + +async def test_setup_entry_without_addon_info(hass, get_addon_discovery_info): + """Test set up entry using OpenZWave add-on but missing discovery info.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="OpenZWave", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + data={"use_addon": True}, + ) + entry.add_to_hass(hass) + + get_addon_discovery_info.return_value = None + + with patch("homeassistant.components.ozw.MQTTClient", autospec=True) as mock_client: + assert not await hass.config_entries.async_setup(entry.entry_id) + + assert mock_client.return_value.start_client.call_count == 0 + assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + + +async def test_unload_entry_with_addon( + hass, get_addon_discovery_info, generic_data, switch_msg, caplog +): + """Test unload the config entry using the OpenZWave add-on.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="OpenZWave", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + data={"use_addon": True}, + ) + entry.add_to_hass(hass) + + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + + with patch("homeassistant.components.ozw.MQTTClient", autospec=True) as mock_client: + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert mock_client.return_value.start_client.call_count == 1 + assert entry.state == config_entries.ENTRY_STATE_LOADED + + await hass.config_entries.async_unload(entry.entry_id) + + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED