Add ozw add-on discovery and mqtt client (#43838)

This commit is contained in:
Martin Hjelmare 2020-12-02 20:03:29 +01:00 committed by GitHub
parent 8efa9c5097
commit 9043b7b214
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 353 additions and 30 deletions

View File

@ -2,6 +2,7 @@
from datetime import timedelta from datetime import timedelta
import logging import logging
import os import os
from typing import Optional
import voluptuous as vol import voluptuous as vol
@ -23,6 +24,7 @@ from homeassistant.util.dt import utcnow
from .addon_panel import async_setup_addon_panel from .addon_panel import async_setup_addon_panel
from .auth import async_setup_auth_view from .auth import async_setup_auth_view
from .const import ATTR_DISCOVERY
from .discovery import async_setup_discovery_view from .discovery import async_setup_discovery_view
from .handler import HassIO, HassioAPIError, api_data from .handler import HassIO, HassioAPIError, api_data
from .http import HassIOView from .http import HassIOView
@ -200,6 +202,17 @@ async def async_set_addon_options(
return await hassio.send_command(command, payload=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 @callback
@bind_hass @bind_hass
def get_info(hass): def get_info(hass):

View File

@ -17,22 +17,25 @@ from openzwavemqtt.const import (
) )
from openzwavemqtt.models.node import OZWNode from openzwavemqtt.models.node import OZWNode
from openzwavemqtt.models.value import OZWValue from openzwavemqtt.models.value import OZWValue
from openzwavemqtt.util.mqtt_client import MQTTClient
import voluptuous as vol import voluptuous as vol
from homeassistant.components import mqtt from homeassistant.components import mqtt
from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.hassio.handler import HassioAPIError
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant, callback 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.device_registry import async_get_registry as get_dev_reg
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from . import const from . import const
from .const import ( from .const import (
CONF_INTEGRATION_CREATED_ADDON, CONF_INTEGRATION_CREATED_ADDON,
CONF_USE_ADDON,
DATA_UNSUBSCRIBE, DATA_UNSUBSCRIBE,
DOMAIN, DOMAIN,
MANAGER, MANAGER,
OPTIONS,
PLATFORMS, PLATFORMS,
TOPIC_OPENZWAVE, TOPIC_OPENZWAVE,
) )
@ -50,13 +53,11 @@ _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
DATA_DEVICES = "zwave-mqtt-devices" DATA_DEVICES = "zwave-mqtt-devices"
DATA_STOP_MQTT_CLIENT = "ozw_stop_mqtt_client"
async def async_setup(hass: HomeAssistant, config: dict): async def async_setup(hass: HomeAssistant, config: dict):
"""Initialize basic config of ozw component.""" """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] = {} hass.data[DOMAIN] = {}
return True return True
@ -69,16 +70,46 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
data_nodes = {} data_nodes = {}
data_values = {} data_values = {}
removed_nodes = [] removed_nodes = []
manager_options = {"topic_prefix": f"{TOPIC_OPENZWAVE}/"}
if entry.unique_id is None:
hass.config_entries.async_update_entry(entry, unique_id=DOMAIN)
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 @callback
def send_message(topic, payload): def send_message(topic, payload):
mqtt.async_publish(hass, topic, json.dumps(payload)) mqtt.async_publish(hass, topic, json.dumps(payload))
options = OZWOptions(send_message=send_message, topic_prefix=f"{TOPIC_OPENZWAVE}/") manager_options["send_message"] = send_message
options = OZWOptions(**manager_options)
manager = OZWManager(options) manager = OZWManager(options)
hass.data[DOMAIN][MANAGER] = manager hass.data[DOMAIN][MANAGER] = manager
hass.data[DOMAIN][OPTIONS] = options
@callback @callback
def async_node_added(node): def async_node_added(node):
@ -234,9 +265,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
for component in PLATFORMS for component in PLATFORMS
] ]
) )
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( ozw_data[DATA_UNSUBSCRIBE].append(
await mqtt.async_subscribe( await mqtt.async_subscribe(
hass, f"{TOPIC_OPENZWAVE}/#", async_receive_message hass, f"{manager.options.topic_prefix}#", async_receive_message
) )
) )
@ -262,6 +311,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
# unsubscribe all listeners # unsubscribe all listeners
for unsubscribe_listener in hass.data[DOMAIN][entry.entry_id][DATA_UNSUBSCRIBE]: for unsubscribe_listener in hass.data[DOMAIN][entry.entry_id][DATA_UNSUBSCRIBE]:
unsubscribe_listener() 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) hass.data[DOMAIN].pop(entry.entry_id)
return True return True

View File

@ -7,7 +7,7 @@ from homeassistant import config_entries
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import AbortFlow 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 from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -16,7 +16,6 @@ CONF_ADDON_DEVICE = "device"
CONF_ADDON_NETWORK_KEY = "network_key" CONF_ADDON_NETWORK_KEY = "network_key"
CONF_NETWORK_KEY = "network_key" CONF_NETWORK_KEY = "network_key"
CONF_USB_PATH = "usb_path" CONF_USB_PATH = "usb_path"
CONF_USE_ADDON = "use_addon"
TITLE = "OpenZWave" TITLE = "OpenZWave"
ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=False): bool}) 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(): if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed") return self.async_abort(reason="single_instance_allowed")
# Currently all flow results need the MQTT integration. # Set a unique_id to make sure discovery flow is aborted on progress.
# This will change when we have the direct MQTT client connection. await self.async_set_unique_id(DOMAIN, raise_on_progress=False)
# 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 not self.hass.components.hassio.is_hassio(): if not self.hass.components.hassio.is_hassio():
return self._async_use_mqtt_integration() return self._async_use_mqtt_integration()
return await self.async_step_on_supervisor() 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): def _async_create_entry_from_vars(self):
"""Return a config entry for the flow.""" """Return a config entry for the flow."""
return self.async_create_entry( 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 This is the entry point for the logic that is needed
when this integration will depend on the MQTT integration. 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() return self._async_create_entry_from_vars()
async def async_step_on_supervisor(self, user_input=None): async def async_step_on_supervisor(self, user_input=None):

View File

@ -12,6 +12,7 @@ DOMAIN = "ozw"
DATA_UNSUBSCRIBE = "unsubscribe" DATA_UNSUBSCRIBE = "unsubscribe"
CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon" CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon"
CONF_USE_ADDON = "use_addon"
PLATFORMS = [ PLATFORMS = [
BINARY_SENSOR_DOMAIN, BINARY_SENSOR_DOMAIN,
@ -24,7 +25,6 @@ PLATFORMS = [
SWITCH_DOMAIN, SWITCH_DOMAIN,
] ]
MANAGER = "manager" MANAGER = "manager"
OPTIONS = "options"
# MQTT Topics # MQTT Topics
TOPIC_OPENZWAVE = "OpenZWave" TOPIC_OPENZWAVE = "OpenZWave"

View File

@ -4,7 +4,7 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ozw", "documentation": "https://www.home-assistant.io/integrations/ozw",
"requirements": [ "requirements": [
"python-openzwave-mqtt==1.3.2" "python-openzwave-mqtt[mqtt-client]==1.4.0"
], ],
"after_dependencies": [ "after_dependencies": [
"mqtt" "mqtt"

View File

@ -11,10 +11,18 @@
}, },
"start_addon": { "start_addon": {
"title": "Enter the OpenZWave add-on configuration", "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": { "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_info_failed": "Failed to get OpenZWave add-on info.",
"addon_install_failed": "Failed to install the OpenZWave add-on.", "addon_install_failed": "Failed to install the OpenZWave add-on.",
"addon_set_config_failed": "Failed to set OpenZWave configuration.", "addon_set_config_failed": "Failed to set OpenZWave configuration.",

View File

@ -4,6 +4,8 @@
"addon_info_failed": "Failed to get OpenZWave add-on info.", "addon_info_failed": "Failed to get OpenZWave add-on info.",
"addon_install_failed": "Failed to install the OpenZWave add-on.", "addon_install_failed": "Failed to install the OpenZWave add-on.",
"addon_set_config_failed": "Failed to set OpenZWave configuration.", "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", "mqtt_required": "The MQTT integration is not set up",
"single_instance_allowed": "Already configured. Only a single configuration possible." "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." "install_addon": "Please wait while the OpenZWave add-on installation finishes. This can take several minutes."
}, },
"step": { "step": {
"hassio_confirm": {
"title": "Set up OpenZWave integration with the OpenZWave add-on"
},
"install_addon": { "install_addon": {
"title": "The OpenZWave add-on installation has started" "title": "The OpenZWave add-on installation has started"
}, },

View File

@ -21,7 +21,7 @@ from homeassistant.components import websocket_api
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv 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 from .lock import ATTR_USERCODE
TYPE = "type" TYPE = "type"
@ -461,7 +461,7 @@ def websocket_refresh_node_info(hass, connection, msg):
"""Tell OpenZWave to re-interview a node.""" """Tell OpenZWave to re-interview a node."""
manager = hass.data[DOMAIN][MANAGER] manager = hass.data[DOMAIN][MANAGER]
options = hass.data[DOMAIN][OPTIONS] options = manager.options
@callback @callback
def forward_node(node): def forward_node(node):

View File

@ -1794,7 +1794,7 @@ python-nest==4.1.0
python-nmap==0.6.1 python-nmap==0.6.1
# homeassistant.components.ozw # homeassistant.components.ozw
python-openzwave-mqtt==1.3.2 python-openzwave-mqtt[mqtt-client]==1.4.0
# homeassistant.components.qbittorrent # homeassistant.components.qbittorrent
python-qbittorrent==0.4.1 python-qbittorrent==0.4.1

View File

@ -884,7 +884,7 @@ python-miio==0.5.4
python-nest==4.1.0 python-nest==4.1.0
# homeassistant.components.ozw # homeassistant.components.ozw
python-openzwave-mqtt==1.3.2 python-openzwave-mqtt[mqtt-client]==1.4.0
# homeassistant.components.songpal # homeassistant.components.songpal
python-songpal==0.12 python-songpal==0.12

View File

@ -253,3 +253,12 @@ def mock_uninstall_addon():
"homeassistant.components.hassio.async_uninstall_addon" "homeassistant.components.hassio.async_uninstall_addon"
) as uninstall_addon: ) as uninstall_addon:
yield 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

View File

@ -9,6 +9,14 @@ from homeassistant.components.ozw.const import DOMAIN
from tests.async_mock import patch from tests.async_mock import patch
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
ADDON_DISCOVERY_INFO = {
"addon": "OpenZWave",
"host": "host1",
"port": 1234,
"username": "name1",
"password": "pass1",
}
@pytest.fixture(name="supervisor") @pytest.fixture(name="supervisor")
def mock_supervisor_fixture(): def mock_supervisor_fixture():
@ -44,7 +52,7 @@ def mock_addon_installed(addon_info):
def mock_addon_options(addon_info): def mock_addon_options(addon_info):
"""Mock add-on options.""" """Mock add-on options."""
addon_info.return_value["options"] = {} addon_info.return_value["options"] = {}
return addon_info return addon_info.return_value["options"]
@pytest.fixture(name="set_addon_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["type"] == "abort"
assert result["reason"] == "addon_install_failed" 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"

View File

@ -5,6 +5,7 @@ from homeassistant.components.ozw import DOMAIN, PLATFORMS, const
from .common import setup_ozw from .common import setup_ozw
from tests.async_mock import patch
from tests.common import MockConfigEntry 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) 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): async def test_unload_entry(hass, generic_data, switch_msg, caplog):
"""Test unload the config entry.""" """Test unload the config entry."""
entry = MockConfigEntry( 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 entry.state == config_entries.ENTRY_STATE_NOT_LOADED
assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert len(hass.config_entries.async_entries(DOMAIN)) == 0
assert "Failed to uninstall the OpenZWave add-on" in caplog.text 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