Install and start Mosquitto MQTT broker add on from MQTT config flow (#124106)
* Opt in to install Mosquitto broker add-on in MQTT config flow * rephrase * Tests with supervisor and running add-on * Complete tests for success flows * Also set up entry in success flow * Use realistic names for addon and broker * Finetuning and fail test cases * Spelling * Improve translation strings * Update addon docstr Co-authored-by: Erik Montnemery <erik@montnemery.com> * Raise AddonError if add-on does not start * Only show the option to use the add-on * Simplify flow, rework and cleanup * Revert unrelated cleanup, process suggestion * Move ADDON_SLUG const to addon module * Move fixture to component level * Move back supervisor fixture * Move addon_setup_time_fixture and superfixe to config flow model tests * Refactor hassio fixture * Rename helpers as they are no fixtures, remove fixture from their names --------- Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
9b9e98a26e
commit
61114d8328
7 changed files with 876 additions and 18 deletions
22
homeassistant/components/mqtt/addon.py
Normal file
22
homeassistant/components/mqtt/addon.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
"""Provide MQTT add-on management.
|
||||
|
||||
Currently only supports the official mosquitto add-on.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.hassio import AddonManager
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
ADDON_SLUG = "core_mosquitto"
|
||||
DATA_ADDON_MANAGER = f"{DOMAIN}_addon_manager"
|
||||
|
||||
|
||||
@singleton(DATA_ADDON_MANAGER)
|
||||
@callback
|
||||
def get_addon_manager(hass: HomeAssistant) -> AddonManager:
|
||||
"""Get the add-on manager."""
|
||||
return AddonManager(hass, LOGGER, "Mosquitto Mqtt Broker", ADDON_SLUG)
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Callable, Mapping
|
||||
import logging
|
||||
import queue
|
||||
from ssl import PROTOCOL_TLS_CLIENT, SSLContext, SSLError
|
||||
from types import MappingProxyType
|
||||
|
@ -14,7 +16,12 @@ from cryptography.x509 import load_pem_x509_certificate
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.file_upload import process_uploaded_file
|
||||
from homeassistant.components.hassio import HassioServiceInfo
|
||||
from homeassistant.components.hassio import HassioServiceInfo, is_hassio
|
||||
from homeassistant.components.hassio.addon_manager import (
|
||||
AddonError,
|
||||
AddonManager,
|
||||
AddonState,
|
||||
)
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
|
@ -32,6 +39,7 @@ from homeassistant.const import (
|
|||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
from homeassistant.helpers.selector import (
|
||||
|
@ -51,6 +59,7 @@ from homeassistant.helpers.selector import (
|
|||
)
|
||||
from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads
|
||||
|
||||
from .addon import get_addon_manager
|
||||
from .client import MqttClientSetup
|
||||
from .const import (
|
||||
ATTR_PAYLOAD,
|
||||
|
@ -91,6 +100,11 @@ from .util import (
|
|||
valid_publish_topic,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ADDON_SETUP_TIMEOUT = 5
|
||||
ADDON_SETUP_TIMEOUT_ROUNDS = 5
|
||||
|
||||
MQTT_TIMEOUT = 5
|
||||
|
||||
ADVANCED_OPTIONS = "advanced_options"
|
||||
|
@ -197,6 +211,12 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
|
||||
entry: ConfigEntry | None
|
||||
_hassio_discovery: dict[str, Any] | None = None
|
||||
_addon_manager: AddonManager
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Set up flow instance."""
|
||||
self.install_task: asyncio.Task | None = None
|
||||
self.start_task: asyncio.Task | None = None
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
|
@ -206,6 +226,118 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
"""Get the options flow for this handler."""
|
||||
return MQTTOptionsFlowHandler(config_entry)
|
||||
|
||||
async def _async_install_addon(self) -> None:
|
||||
"""Install the Mosquitto Mqtt broker add-on."""
|
||||
addon_manager: AddonManager = get_addon_manager(self.hass)
|
||||
await addon_manager.async_schedule_install_addon()
|
||||
|
||||
async def async_step_install_failed(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Add-on installation failed."""
|
||||
return self.async_abort(
|
||||
reason="addon_install_failed",
|
||||
description_placeholders={"addon": self._addon_manager.addon_name},
|
||||
)
|
||||
|
||||
async def async_step_install_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Install Mosquitto Broker add-on."""
|
||||
if self.install_task is None:
|
||||
self.install_task = self.hass.async_create_task(self._async_install_addon())
|
||||
|
||||
if not self.install_task.done():
|
||||
return self.async_show_progress(
|
||||
step_id="install_addon",
|
||||
progress_action="install_addon",
|
||||
progress_task=self.install_task,
|
||||
)
|
||||
|
||||
try:
|
||||
await self.install_task
|
||||
except AddonError as err:
|
||||
_LOGGER.error(err)
|
||||
return self.async_show_progress_done(next_step_id="install_failed")
|
||||
finally:
|
||||
self.install_task = None
|
||||
|
||||
return self.async_show_progress_done(next_step_id="start_addon")
|
||||
|
||||
async def async_step_start_failed(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Add-on start failed."""
|
||||
return self.async_abort(
|
||||
reason="addon_start_failed",
|
||||
description_placeholders={"addon": self._addon_manager.addon_name},
|
||||
)
|
||||
|
||||
async def async_step_start_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Start Mosquitto Broker add-on."""
|
||||
if not self.start_task:
|
||||
self.start_task = self.hass.async_create_task(self._async_start_addon())
|
||||
if not self.start_task.done():
|
||||
return self.async_show_progress(
|
||||
step_id="start_addon",
|
||||
progress_action="start_addon",
|
||||
progress_task=self.start_task,
|
||||
)
|
||||
try:
|
||||
await self.start_task
|
||||
except AddonError as err:
|
||||
_LOGGER.error(err)
|
||||
return self.async_show_progress_done(next_step_id="start_failed")
|
||||
finally:
|
||||
self.start_task = None
|
||||
|
||||
return self.async_show_progress_done(next_step_id="setup_entry_from_discovery")
|
||||
|
||||
async def _async_get_config_and_try(self) -> dict[str, Any] | None:
|
||||
"""Get the MQTT add-on discovery info and try the connection."""
|
||||
if self._hassio_discovery is not None:
|
||||
return self._hassio_discovery
|
||||
addon_manager: AddonManager = get_addon_manager(self.hass)
|
||||
try:
|
||||
addon_discovery_config = (
|
||||
await addon_manager.async_get_addon_discovery_info()
|
||||
)
|
||||
config: dict[str, Any] = {
|
||||
CONF_BROKER: addon_discovery_config[CONF_HOST],
|
||||
CONF_PORT: addon_discovery_config[CONF_PORT],
|
||||
CONF_USERNAME: addon_discovery_config.get(CONF_USERNAME),
|
||||
CONF_PASSWORD: addon_discovery_config.get(CONF_PASSWORD),
|
||||
CONF_DISCOVERY: DEFAULT_DISCOVERY,
|
||||
}
|
||||
except AddonError:
|
||||
# We do not have discovery information yet
|
||||
return None
|
||||
if await self.hass.async_add_executor_job(
|
||||
try_connection,
|
||||
config,
|
||||
):
|
||||
self._hassio_discovery = config
|
||||
return config
|
||||
return None
|
||||
|
||||
async def _async_start_addon(self) -> None:
|
||||
"""Start the Mosquitto Broker add-on."""
|
||||
addon_manager: AddonManager = get_addon_manager(self.hass)
|
||||
await addon_manager.async_schedule_start_addon()
|
||||
|
||||
# Sleep some seconds to let the add-on start properly before connecting.
|
||||
for _ in range(ADDON_SETUP_TIMEOUT_ROUNDS):
|
||||
await asyncio.sleep(ADDON_SETUP_TIMEOUT)
|
||||
# Finish setup using discovery info to test the connection
|
||||
if await self._async_get_config_and_try():
|
||||
break
|
||||
else:
|
||||
raise AddonError(
|
||||
f"Failed to correctly start {addon_manager.addon_name} add-on"
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
@ -213,8 +345,57 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
if is_hassio(self.hass):
|
||||
# Offer to set up broker add-on if supervisor is available
|
||||
self._addon_manager = get_addon_manager(self.hass)
|
||||
return self.async_show_menu(
|
||||
step_id="user",
|
||||
menu_options=["addon", "broker"],
|
||||
description_placeholders={"addon": self._addon_manager.addon_name},
|
||||
)
|
||||
|
||||
# Start up a flow for manual setup
|
||||
return await self.async_step_broker()
|
||||
|
||||
async def async_step_setup_entry_from_discovery(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Set up mqtt entry from discovery info."""
|
||||
if (config := await self._async_get_config_and_try()) is not None:
|
||||
return self.async_create_entry(
|
||||
title=self._addon_manager.addon_name,
|
||||
data=config,
|
||||
)
|
||||
|
||||
raise AbortFlow(
|
||||
"addon_connection_failed",
|
||||
description_placeholders={"addon": self._addon_manager.addon_name},
|
||||
)
|
||||
|
||||
async def async_step_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Install and start MQTT broker add-on."""
|
||||
addon_manager = self._addon_manager
|
||||
|
||||
try:
|
||||
addon_info = await addon_manager.async_get_addon_info()
|
||||
except AddonError as err:
|
||||
raise AbortFlow(
|
||||
"addon_info_failed",
|
||||
description_placeholders={"addon": self._addon_manager.addon_name},
|
||||
) from err
|
||||
|
||||
if addon_info.state == AddonState.RUNNING:
|
||||
# Finish setup using discovery info
|
||||
return await self.async_step_setup_entry_from_discovery()
|
||||
|
||||
if addon_info.state == AddonState.NOT_RUNNING:
|
||||
return await self.async_step_start_addon()
|
||||
|
||||
# Install the add-on and start it
|
||||
return await self.async_step_install_addon()
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
|
@ -293,7 +474,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
async def async_step_hassio(
|
||||
self, discovery_info: HassioServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Receive a Hass.io discovery."""
|
||||
"""Receive a Hass.io discovery or process setup after addon install."""
|
||||
await self._async_handle_discovery_without_unique_id()
|
||||
|
||||
self._hassio_discovery = discovery_info.config
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
"""Constants used by multiple MQTT modules."""
|
||||
|
||||
import logging
|
||||
|
||||
import jinja2
|
||||
|
||||
from homeassistant.const import CONF_PAYLOAD, Platform
|
||||
|
@ -148,6 +150,7 @@ DEFAULT_WILL = {
|
|||
}
|
||||
|
||||
DOMAIN = "mqtt"
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
MQTT_CONNECTION_STATE = "mqtt_connection_state"
|
||||
|
||||
|
|
|
@ -23,6 +23,13 @@
|
|||
},
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Please choose how you want to connect to the MQTT broker:",
|
||||
"menu_options": {
|
||||
"addon": "Use the official {addon} add-on.",
|
||||
"broker": "Manually enter the MQTT broker connection details"
|
||||
}
|
||||
},
|
||||
"broker": {
|
||||
"description": "Please enter the connection information of your MQTT broker.",
|
||||
"data": {
|
||||
|
@ -63,6 +70,12 @@
|
|||
"ws_path": "The WebSocket path to be used for the connection to your MQTT broker."
|
||||
}
|
||||
},
|
||||
"install_addon": {
|
||||
"title": "Installing add-on"
|
||||
},
|
||||
"start_addon": {
|
||||
"title": "Starting add-on"
|
||||
},
|
||||
"hassio_confirm": {
|
||||
"title": "MQTT Broker via Home Assistant add-on",
|
||||
"description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the add-on {addon}?",
|
||||
|
@ -87,6 +100,10 @@
|
|||
}
|
||||
},
|
||||
"abort": {
|
||||
"addon_info_failed": "Failed get info for the {addon} add-on.",
|
||||
"addon_install_failed": "Failed to install the {addon} add-on.",
|
||||
"addon_start_failed": "Failed to start the {addon} add-on.",
|
||||
"addon_connection_failed": "Failed to connect to the {addon} add-on. Check the add-on status and try again later.",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||
|
|
|
@ -6,7 +6,7 @@ from collections.abc import Callable, Generator
|
|||
from importlib.util import find_spec
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -180,3 +180,92 @@ def mock_legacy_device_tracker_setup() -> Callable[[HomeAssistant, MockScanner],
|
|||
from .device_tracker.common import mock_legacy_device_tracker_setup
|
||||
|
||||
return mock_legacy_device_tracker_setup
|
||||
|
||||
|
||||
@pytest.fixture(name="discovery_info")
|
||||
def discovery_info_fixture() -> Any:
|
||||
"""Return the discovery info from the supervisor."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .hassio.common import mock_discovery_info
|
||||
|
||||
return mock_discovery_info()
|
||||
|
||||
|
||||
@pytest.fixture(name="get_addon_discovery_info")
|
||||
def get_addon_discovery_info_fixture(discovery_info: Any) -> Generator[AsyncMock]:
|
||||
"""Mock get add-on discovery info."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .hassio.common import mock_get_addon_discovery_info
|
||||
|
||||
yield from mock_get_addon_discovery_info(discovery_info)
|
||||
|
||||
|
||||
@pytest.fixture(name="addon_store_info")
|
||||
def addon_store_info_fixture() -> Generator[AsyncMock]:
|
||||
"""Mock Supervisor add-on store info."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .hassio.common import mock_addon_store_info
|
||||
|
||||
yield from mock_addon_store_info()
|
||||
|
||||
|
||||
@pytest.fixture(name="addon_info")
|
||||
def addon_info_fixture() -> Generator[AsyncMock]:
|
||||
"""Mock Supervisor add-on info."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .hassio.common import mock_addon_info
|
||||
|
||||
yield from mock_addon_info()
|
||||
|
||||
|
||||
@pytest.fixture(name="addon_not_installed")
|
||||
def addon_not_installed_fixture(
|
||||
addon_store_info: AsyncMock, addon_info: AsyncMock
|
||||
) -> AsyncMock:
|
||||
"""Mock add-on not installed."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .hassio.common import mock_addon_not_installed
|
||||
|
||||
return mock_addon_not_installed(addon_store_info, addon_info)
|
||||
|
||||
|
||||
@pytest.fixture(name="addon_installed")
|
||||
def addon_installed_fixture(
|
||||
addon_store_info: AsyncMock, addon_info: AsyncMock
|
||||
) -> AsyncMock:
|
||||
"""Mock add-on already installed but not running."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .hassio.common import mock_addon_installed
|
||||
|
||||
return mock_addon_installed(addon_store_info, addon_info)
|
||||
|
||||
|
||||
@pytest.fixture(name="addon_running")
|
||||
def addon_running_fixture(
|
||||
addon_store_info: AsyncMock, addon_info: AsyncMock
|
||||
) -> AsyncMock:
|
||||
"""Mock add-on already running."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .hassio.common import mock_addon_running
|
||||
|
||||
return mock_addon_running(addon_store_info, addon_info)
|
||||
|
||||
|
||||
@pytest.fixture(name="install_addon")
|
||||
def install_addon_fixture(
|
||||
addon_store_info: AsyncMock, addon_info: AsyncMock
|
||||
) -> Generator[AsyncMock]:
|
||||
"""Mock install add-on."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .hassio.common import mock_install_addon
|
||||
|
||||
yield from mock_install_addon(addon_store_info, addon_info)
|
||||
|
||||
|
||||
@pytest.fixture(name="start_addon")
|
||||
def start_addon_fixture() -> Generator[AsyncMock]:
|
||||
"""Mock start add-on."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .hassio.common import mock_start_addon
|
||||
|
||||
yield from mock_start_addon()
|
||||
|
|
125
tests/components/hassio/common.py
Normal file
125
tests/components/hassio/common.py
Normal file
|
@ -0,0 +1,125 @@
|
|||
"""Provide common test tools for hassio."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
from unittest.mock import DEFAULT, AsyncMock, patch
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
def mock_discovery_info() -> Any:
|
||||
"""Return the discovery info from the supervisor."""
|
||||
return DEFAULT
|
||||
|
||||
|
||||
def mock_get_addon_discovery_info(discovery_info: Any) -> Generator[AsyncMock]:
|
||||
"""Mock get add-on discovery info."""
|
||||
with patch(
|
||||
"homeassistant.components.hassio.addon_manager.async_get_addon_discovery_info",
|
||||
return_value=discovery_info,
|
||||
) as get_addon_discovery_info:
|
||||
yield get_addon_discovery_info
|
||||
|
||||
|
||||
def mock_addon_store_info() -> Generator[AsyncMock]:
|
||||
"""Mock Supervisor add-on store info."""
|
||||
with patch(
|
||||
"homeassistant.components.hassio.addon_manager.async_get_addon_store_info"
|
||||
) as addon_store_info:
|
||||
addon_store_info.return_value = {
|
||||
"available": False,
|
||||
"installed": None,
|
||||
"state": None,
|
||||
"version": "1.0.0",
|
||||
}
|
||||
yield addon_store_info
|
||||
|
||||
|
||||
def mock_addon_info() -> Generator[AsyncMock]:
|
||||
"""Mock Supervisor add-on info."""
|
||||
with patch(
|
||||
"homeassistant.components.hassio.addon_manager.async_get_addon_info",
|
||||
) as addon_info:
|
||||
addon_info.return_value = {
|
||||
"available": False,
|
||||
"hostname": None,
|
||||
"options": {},
|
||||
"state": None,
|
||||
"update_available": False,
|
||||
"version": None,
|
||||
}
|
||||
yield addon_info
|
||||
|
||||
|
||||
def mock_addon_not_installed(
|
||||
addon_store_info: AsyncMock, addon_info: AsyncMock
|
||||
) -> AsyncMock:
|
||||
"""Mock add-on not installed."""
|
||||
addon_store_info.return_value["available"] = True
|
||||
return addon_info
|
||||
|
||||
|
||||
def mock_addon_installed(
|
||||
addon_store_info: AsyncMock, addon_info: AsyncMock
|
||||
) -> AsyncMock:
|
||||
"""Mock add-on already installed but not running."""
|
||||
addon_store_info.return_value = {
|
||||
"available": True,
|
||||
"installed": "1.0.0",
|
||||
"state": "stopped",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
addon_info.return_value["available"] = True
|
||||
addon_info.return_value["hostname"] = "core-matter-server"
|
||||
addon_info.return_value["state"] = "stopped"
|
||||
addon_info.return_value["version"] = "1.0.0"
|
||||
return addon_info
|
||||
|
||||
|
||||
def mock_addon_running(addon_store_info: AsyncMock, addon_info: AsyncMock) -> AsyncMock:
|
||||
"""Mock add-on already running."""
|
||||
addon_store_info.return_value = {
|
||||
"available": True,
|
||||
"installed": "1.0.0",
|
||||
"state": "started",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
addon_info.return_value["available"] = True
|
||||
addon_info.return_value["hostname"] = "core-mosquitto"
|
||||
addon_info.return_value["state"] = "started"
|
||||
addon_info.return_value["version"] = "1.0.0"
|
||||
return addon_info
|
||||
|
||||
|
||||
def mock_install_addon(
|
||||
addon_store_info: AsyncMock, addon_info: AsyncMock
|
||||
) -> Generator[AsyncMock]:
|
||||
"""Mock install add-on."""
|
||||
|
||||
async def install_addon_side_effect(hass: HomeAssistant, slug: str) -> None:
|
||||
"""Mock install add-on."""
|
||||
addon_store_info.return_value = {
|
||||
"available": True,
|
||||
"installed": "1.0.0",
|
||||
"state": "stopped",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
addon_info.return_value["available"] = True
|
||||
addon_info.return_value["state"] = "stopped"
|
||||
addon_info.return_value["version"] = "1.0.0"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.hassio.addon_manager.async_install_addon"
|
||||
) as install_addon:
|
||||
install_addon.side_effect = install_addon_side_effect
|
||||
yield install_addon
|
||||
|
||||
|
||||
def mock_start_addon() -> Generator[AsyncMock]:
|
||||
"""Mock start add-on."""
|
||||
with patch(
|
||||
"homeassistant.components.hassio.addon_manager.async_start_addon"
|
||||
) as start_addon:
|
||||
yield start_addon
|
|
@ -14,6 +14,8 @@ import voluptuous as vol
|
|||
from homeassistant import config_entries
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.components.hassio import HassioServiceInfo
|
||||
from homeassistant.components.hassio.addon_manager import AddonError
|
||||
from homeassistant.components.hassio.handler import HassioAPIError
|
||||
from homeassistant.components.mqtt.config_flow import PWD_NOT_CHANGED
|
||||
from homeassistant.const import (
|
||||
CONF_CLIENT_ID,
|
||||
|
@ -28,6 +30,15 @@ from homeassistant.data_entry_flow import FlowResultType
|
|||
from tests.common import MockConfigEntry
|
||||
from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient
|
||||
|
||||
ADD_ON_DISCOVERY_INFO = {
|
||||
"addon": "Mosquitto Mqtt Broker",
|
||||
"host": "core-mosquitto",
|
||||
"port": 1883,
|
||||
"username": "mock-user",
|
||||
"password": "mock-pass",
|
||||
"protocol": "3.1.1",
|
||||
"ssl": False,
|
||||
}
|
||||
MOCK_CLIENT_CERT = b"## mock client certificate file ##"
|
||||
MOCK_CLIENT_KEY = b"## mock key file ##"
|
||||
|
||||
|
@ -186,6 +197,29 @@ def mock_process_uploaded_file(
|
|||
yield mock_upload
|
||||
|
||||
|
||||
@pytest.fixture(name="supervisor")
|
||||
def supervisor_fixture() -> Generator[MagicMock]:
|
||||
"""Mock Supervisor."""
|
||||
with patch(
|
||||
"homeassistant.components.mqtt.config_flow.is_hassio", return_value=True
|
||||
) as is_hassio:
|
||||
yield is_hassio
|
||||
|
||||
|
||||
@pytest.fixture(name="addon_setup_time", autouse=True)
|
||||
def addon_setup_time_fixture() -> Generator[int]:
|
||||
"""Mock add-on setup sleep time."""
|
||||
with patch(
|
||||
"homeassistant.components.mqtt.config_flow.ADDON_SETUP_TIMEOUT", new=0
|
||||
) as addon_setup_time:
|
||||
yield addon_setup_time
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_get_addon_discovery_info(get_addon_discovery_info: AsyncMock) -> None:
|
||||
"""Mock get add-on discovery info."""
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mqtt_client_mock")
|
||||
async def test_user_connection_works(
|
||||
hass: HomeAssistant,
|
||||
|
@ -216,6 +250,47 @@ async def test_user_connection_works(
|
|||
assert len(mock_finish_setup.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mqtt_client_mock", "supervisor")
|
||||
async def test_user_connection_works_with_supervisor(
|
||||
hass: HomeAssistant,
|
||||
mock_try_connection: MagicMock,
|
||||
mock_finish_setup: MagicMock,
|
||||
) -> None:
|
||||
"""Test we can finish a config flow with a supervised install."""
|
||||
mock_try_connection.return_value = True
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"mqtt", context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["menu_options"] == ["addon", "broker"]
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "broker"},
|
||||
)
|
||||
|
||||
# Assert a manual setup flow
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"broker": "127.0.0.1"}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["result"].data == {
|
||||
"broker": "127.0.0.1",
|
||||
"port": 1883,
|
||||
"discovery": True,
|
||||
}
|
||||
# Check we tried the connection
|
||||
assert len(mock_try_connection.mock_calls) == 1
|
||||
# Check config entry got setup
|
||||
assert len(mock_finish_setup.mock_calls) == 1
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mqtt_client_mock")
|
||||
async def test_user_v5_connection_works(
|
||||
hass: HomeAssistant,
|
||||
|
@ -382,16 +457,8 @@ async def test_hassio_confirm(
|
|||
result = await hass.config_entries.flow.async_init(
|
||||
"mqtt",
|
||||
data=HassioServiceInfo(
|
||||
config={
|
||||
"addon": "Mock Addon",
|
||||
"host": "mock-broker",
|
||||
"port": 1883,
|
||||
"username": "mock-user",
|
||||
"password": "mock-pass",
|
||||
"protocol": "3.1.1", # Set by the addon's discovery, ignored by HA
|
||||
"ssl": False, # Set by the addon's discovery, ignored by HA
|
||||
},
|
||||
name="Mock Addon",
|
||||
config=ADD_ON_DISCOVERY_INFO.copy(),
|
||||
name="Mosquitto Mqtt Broker",
|
||||
slug="mosquitto",
|
||||
uuid="1234",
|
||||
),
|
||||
|
@ -399,7 +466,7 @@ async def test_hassio_confirm(
|
|||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "hassio_confirm"
|
||||
assert result["description_placeholders"] == {"addon": "Mock Addon"}
|
||||
assert result["description_placeholders"] == {"addon": "Mosquitto Mqtt Broker"}
|
||||
|
||||
mock_try_connection_success.reset_mock()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
|
@ -408,7 +475,7 @@ async def test_hassio_confirm(
|
|||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["result"].data == {
|
||||
"broker": "mock-broker",
|
||||
"broker": "core-mosquitto",
|
||||
"port": 1883,
|
||||
"username": "mock-user",
|
||||
"password": "mock-pass",
|
||||
|
@ -426,14 +493,12 @@ async def test_hassio_cannot_connect(
|
|||
mock_finish_setup: MagicMock,
|
||||
) -> None:
|
||||
"""Test a config flow is aborted when a connection was not successful."""
|
||||
mock_try_connection.return_value = True
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"mqtt",
|
||||
data=HassioServiceInfo(
|
||||
config={
|
||||
"addon": "Mock Addon",
|
||||
"host": "mock-broker",
|
||||
"host": "core-mosquitto",
|
||||
"port": 1883,
|
||||
"username": "mock-user",
|
||||
"password": "mock-pass",
|
||||
|
@ -463,6 +528,362 @@ async def test_hassio_cannot_connect(
|
|||
assert len(mock_finish_setup.mock_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"mqtt_client_mock", "supervisor", "addon_info", "addon_running"
|
||||
)
|
||||
@pytest.mark.parametrize("discovery_info", [{"config": ADD_ON_DISCOVERY_INFO.copy()}])
|
||||
async def test_addon_flow_with_supervisor_addon_running(
|
||||
hass: HomeAssistant,
|
||||
mock_try_connection_success: MagicMock,
|
||||
mock_finish_setup: MagicMock,
|
||||
) -> None:
|
||||
"""Test we perform an auto config flow with a supervised install.
|
||||
|
||||
Case: The Mosquitto add-on is already installed, and running.
|
||||
"""
|
||||
# show menu
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"mqtt", context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["menu_options"] == ["addon", "broker"]
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
# select install via add-on
|
||||
mock_try_connection_success.reset_mock()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "addon"},
|
||||
)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["result"].data == {
|
||||
"broker": "core-mosquitto",
|
||||
"port": 1883,
|
||||
"username": "mock-user",
|
||||
"password": "mock-pass",
|
||||
"discovery": True,
|
||||
}
|
||||
# Check we tried the connection
|
||||
assert len(mock_try_connection_success.mock_calls)
|
||||
# Check config entry got setup
|
||||
assert len(mock_finish_setup.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"mqtt_client_mock", "supervisor", "addon_info", "addon_installed", "start_addon"
|
||||
)
|
||||
@pytest.mark.parametrize("discovery_info", [{"config": ADD_ON_DISCOVERY_INFO.copy()}])
|
||||
async def test_addon_flow_with_supervisor_addon_installed(
|
||||
hass: HomeAssistant,
|
||||
mock_try_connection_success: MagicMock,
|
||||
mock_finish_setup: MagicMock,
|
||||
) -> None:
|
||||
"""Test we perform an auto config flow with a supervised install.
|
||||
|
||||
Case: The Mosquitto add-on is installed, but not running.
|
||||
"""
|
||||
# show menu
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"mqtt", context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["menu_options"] == ["addon", "broker"]
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
# select install via add-on
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "addon"},
|
||||
)
|
||||
|
||||
# add-on installed but not started, so we wait for start-up
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["progress_action"] == "start_addon"
|
||||
assert result["step_id"] == "start_addon"
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
mock_try_connection_success.reset_mock()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "start_addon"},
|
||||
)
|
||||
|
||||
# add-on is running, so entry can be installed
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["result"].data == {
|
||||
"broker": "core-mosquitto",
|
||||
"port": 1883,
|
||||
"username": "mock-user",
|
||||
"password": "mock-pass",
|
||||
"discovery": True,
|
||||
}
|
||||
# Check we tried the connection
|
||||
assert len(mock_try_connection_success.mock_calls)
|
||||
# Check config entry got setup
|
||||
assert len(mock_finish_setup.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"mqtt_client_mock", "supervisor", "addon_info", "addon_running"
|
||||
)
|
||||
@pytest.mark.parametrize("discovery_info", [{"config": ADD_ON_DISCOVERY_INFO.copy()}])
|
||||
async def test_addon_flow_with_supervisor_addon_running_connection_fails(
|
||||
hass: HomeAssistant,
|
||||
mock_try_connection: MagicMock,
|
||||
) -> None:
|
||||
"""Test we perform an auto config flow with a supervised install.
|
||||
|
||||
Case: The Mosquitto add-on is already installed, and running.
|
||||
"""
|
||||
# show menu
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"mqtt", context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["menu_options"] == ["addon", "broker"]
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
# select install via add-on but the connection fails and the flow will be aborted.
|
||||
mock_try_connection.return_value = False
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "addon"},
|
||||
)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"mqtt_client_mock",
|
||||
"supervisor",
|
||||
"addon_info",
|
||||
"addon_installed",
|
||||
)
|
||||
async def test_addon_not_running_api_error(
|
||||
hass: HomeAssistant,
|
||||
start_addon: AsyncMock,
|
||||
) -> None:
|
||||
"""Test we perform an auto config flow with a supervised install.
|
||||
|
||||
Case: The Mosquitto add-on start fails on a API error.
|
||||
"""
|
||||
start_addon.side_effect = HassioAPIError()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"mqtt", context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["menu_options"] == ["addon", "broker"]
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "addon"},
|
||||
)
|
||||
# add-on not installed, so we wait for install
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["progress_action"] == "start_addon"
|
||||
assert result["step_id"] == "start_addon"
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "install_addon"},
|
||||
)
|
||||
|
||||
# add-on start-up failed
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "addon_start_failed"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"mqtt_client_mock",
|
||||
"supervisor",
|
||||
"start_addon",
|
||||
"addon_installed",
|
||||
)
|
||||
async def test_addon_discovery_info_error(
|
||||
hass: HomeAssistant,
|
||||
addon_info: AsyncMock,
|
||||
get_addon_discovery_info: AsyncMock,
|
||||
) -> None:
|
||||
"""Test we perform an auto config flow with a supervised install.
|
||||
|
||||
Case: The Mosquitto add-on start on a discovery error.
|
||||
"""
|
||||
get_addon_discovery_info.side_effect = AddonError
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"mqtt", context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["menu_options"] == ["addon", "broker"]
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "addon"},
|
||||
)
|
||||
# Addon will retry
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["progress_action"] == "start_addon"
|
||||
assert result["step_id"] == "start_addon"
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "start_addon"},
|
||||
)
|
||||
|
||||
# add-on start-up failed
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "addon_start_failed"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"mqtt_client_mock",
|
||||
"supervisor",
|
||||
"start_addon",
|
||||
"addon_installed",
|
||||
)
|
||||
async def test_addon_info_error(
|
||||
hass: HomeAssistant,
|
||||
addon_info: AsyncMock,
|
||||
) -> None:
|
||||
"""Test we perform an auto config flow with a supervised install.
|
||||
|
||||
Case: The Mosquitto add-on info could not be retrieved.
|
||||
"""
|
||||
addon_info.side_effect = AddonError()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"mqtt", context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["menu_options"] == ["addon", "broker"]
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "addon"},
|
||||
)
|
||||
|
||||
# add-on info failed
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "addon_info_failed"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"mqtt_client_mock",
|
||||
"supervisor",
|
||||
"addon_info",
|
||||
"addon_not_installed",
|
||||
"install_addon",
|
||||
"start_addon",
|
||||
)
|
||||
@pytest.mark.parametrize("discovery_info", [{"config": ADD_ON_DISCOVERY_INFO.copy()}])
|
||||
async def test_addon_flow_with_supervisor_addon_not_installed(
|
||||
hass: HomeAssistant,
|
||||
mock_try_connection_success: MagicMock,
|
||||
mock_finish_setup: MagicMock,
|
||||
) -> None:
|
||||
"""Test we perform an auto config flow with a supervised install.
|
||||
|
||||
Case: The Mosquitto add-on is not yet installed nor running.
|
||||
"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"mqtt", context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["menu_options"] == ["addon", "broker"]
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "addon"},
|
||||
)
|
||||
# add-on not installed, so we wait for install
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["progress_action"] == "install_addon"
|
||||
assert result["step_id"] == "install_addon"
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "install_addon"},
|
||||
)
|
||||
|
||||
# add-on installed but not started, so we wait for start-up
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["progress_action"] == "start_addon"
|
||||
assert result["step_id"] == "start_addon"
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
mock_try_connection_success.reset_mock()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "start_addon"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["result"].data == {
|
||||
"broker": "core-mosquitto",
|
||||
"port": 1883,
|
||||
"username": "mock-user",
|
||||
"password": "mock-pass",
|
||||
"discovery": True,
|
||||
}
|
||||
# Check we tried the connection
|
||||
assert len(mock_try_connection_success.mock_calls)
|
||||
# Check config entry got setup
|
||||
assert len(mock_finish_setup.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"mqtt_client_mock",
|
||||
"supervisor",
|
||||
"addon_info",
|
||||
"addon_not_installed",
|
||||
"start_addon",
|
||||
)
|
||||
async def test_addon_not_installed_failures(
|
||||
hass: HomeAssistant,
|
||||
install_addon: AsyncMock,
|
||||
) -> None:
|
||||
"""Test we perform an auto config flow with a supervised install.
|
||||
|
||||
Case: The Mosquitto add-on install fails.
|
||||
"""
|
||||
install_addon.side_effect = HassioAPIError()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"mqtt", context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["menu_options"] == ["addon", "broker"]
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "addon"},
|
||||
)
|
||||
# add-on not installed, so we wait for install
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["progress_action"] == "install_addon"
|
||||
assert result["step_id"] == "install_addon"
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "install_addon"},
|
||||
)
|
||||
|
||||
# add-on install failed
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "addon_install_failed"
|
||||
|
||||
|
||||
async def test_option_flow(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
|
|
Loading…
Add table
Reference in a new issue