diff --git a/homeassistant/components/mealie/config_flow.py b/homeassistant/components/mealie/config_flow.py index 2addd23284ee..25e46ec6262e 100644 --- a/homeassistant/components/mealie/config_flow.py +++ b/homeassistant/components/mealie/config_flow.py @@ -7,8 +7,9 @@ from aiomealie import MealieAuthenticationError, MealieClient, MealieConnectionE import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_VERIFY_SSL +from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_PORT, CONF_VERIFY_SSL from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.service_info.hassio import HassioServiceInfo from .const import DOMAIN, LOGGER, MIN_REQUIRED_MEALIE_VERSION from .utils import create_version @@ -25,13 +26,21 @@ REAUTH_SCHEMA = vol.Schema( vol.Required(CONF_API_TOKEN): str, } ) +DISCOVERY_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_TOKEN): str, + } +) class MealieConfigFlow(ConfigFlow, domain=DOMAIN): """Mealie config flow.""" + VERSION = 1 + host: str | None = None verify_ssl: bool = True + _hassio_discovery: dict[str, Any] | None = None async def check_connection( self, api_token: str @@ -143,3 +152,59 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN): data_schema=USER_SCHEMA, errors=errors, ) + + async def async_step_hassio( + self, discovery_info: HassioServiceInfo + ) -> ConfigFlowResult: + """Prepare configuration for a Mealie add-on. + + This flow is triggered by the discovery component. + """ + await self._async_handle_discovery_without_unique_id() + + self._hassio_discovery = discovery_info.config + + return await self.async_step_hassio_confirm() + + async def async_step_hassio_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm Supervisor discovery and prompt for API token.""" + if user_input is None: + return await self._show_hassio_form() + + assert self._hassio_discovery + + self.host = ( + f"{self._hassio_discovery[CONF_HOST]}:{self._hassio_discovery[CONF_PORT]}" + ) + self.verify_ssl = True + + errors, user_id = await self.check_connection( + user_input[CONF_API_TOKEN], + ) + + if not errors: + await self.async_set_unique_id(user_id) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title="Mealie", + data={ + CONF_HOST: self.host, + CONF_API_TOKEN: user_input[CONF_API_TOKEN], + CONF_VERIFY_SSL: self.verify_ssl, + }, + ) + return await self._show_hassio_form(errors) + + async def _show_hassio_form( + self, errors: dict[str, str] | None = None + ) -> ConfigFlowResult: + """Show the Hass.io confirmation form to the user.""" + assert self._hassio_discovery + return self.async_show_form( + step_id="hassio_confirm", + data_schema=DISCOVERY_SCHEMA, + description_placeholders={"addon": self._hassio_discovery["addon"]}, + errors=errors or {}, + ) diff --git a/homeassistant/components/mealie/quality_scale.yaml b/homeassistant/components/mealie/quality_scale.yaml index 738c5b99d911..93fb3ae74a02 100644 --- a/homeassistant/components/mealie/quality_scale.yaml +++ b/homeassistant/components/mealie/quality_scale.yaml @@ -39,8 +39,14 @@ rules: # Gold devices: done diagnostics: done - discovery-update-info: todo - discovery: todo + discovery-update-info: + status: exempt + comment: | + This integration will only discover a Mealie addon that is local, not on the network. + discovery: + status: done + comment: | + The integration will discover a Mealie addon posting a discovery message. docs-data-update: done docs-examples: done docs-known-limitations: todo diff --git a/homeassistant/components/mealie/strings.json b/homeassistant/components/mealie/strings.json index 5533631f7556..8e51da6d7d11 100644 --- a/homeassistant/components/mealie/strings.json +++ b/homeassistant/components/mealie/strings.json @@ -39,6 +39,16 @@ "api_token": "[%key:component::mealie::common::data_description_api_token%]", "verify_ssl": "[%key:component::mealie::common::data_description_verify_ssl%]" } + }, + "hassio_confirm": { + "title": "Mealie via Home Assistant add-on", + "description": "Do you want to configure Home Assistant to connect to the Mealie instance provided by the add-on: {addon}?", + "data": { + "api_token": "[%key:common::config_flow::data::api_token%]" + }, + "data_description": { + "api_token": "[%key:component::mealie::common::data_description_api_token%]" + } } }, "error": { @@ -50,6 +60,7 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "wrong_account": "You have to use the same account that was used to configure the integration." diff --git a/tests/components/mealie/test_config_flow.py b/tests/components/mealie/test_config_flow.py index 628f0290f43e..f86818a933f0 100644 --- a/tests/components/mealie/test_config_flow.py +++ b/tests/components/mealie/test_config_flow.py @@ -6,10 +6,11 @@ from aiomealie import About, MealieAuthenticationError, MealieConnectionError import pytest from homeassistant.components.mealie.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_HASSIO, SOURCE_IGNORE, SOURCE_USER from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.hassio import HassioServiceInfo from . import setup_integration @@ -361,3 +362,137 @@ async def test_reconfigure_flow_exceptions( ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" + + +async def test_hassio_success( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test successful Supervisor flow.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=HassioServiceInfo( + config={"addon": "Mealie", "host": "http://test", "port": 9090}, + name="mealie", + slug="mealie", + uuid="1234", + ), + context={"source": SOURCE_HASSIO}, + ) + + assert result.get("type") is FlowResultType.FORM + assert result.get("step_id") == "hassio_confirm" + assert result.get("description_placeholders") == {"addon": "Mealie"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_API_TOKEN: "token"} + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Mealie" + assert result["data"] == { + CONF_HOST: "http://test:9090", + CONF_API_TOKEN: "token", + CONF_VERIFY_SSL: True, + } + assert result["result"].unique_id == "bf1c62fe-4941-4332-9886-e54e88dbdba0" + + +async def test_hassio_already_configured( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test we only allow a single config flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=HassioServiceInfo( + config={ + "addon": "Mealie", + "host": "mock-mealie", + "port": "9090", + }, + name="Mealie", + slug="mealie", + uuid="1234", + ), + context={"source": SOURCE_HASSIO}, + ) + assert result + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_hassio_ignored(hass: HomeAssistant) -> None: + """Test the supervisor discovered instance can be ignored.""" + MockConfigEntry(domain=DOMAIN, source=SOURCE_IGNORE).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=HassioServiceInfo( + config={ + "addon": "Mealie", + "host": "mock-mealie", + "port": "9090", + }, + name="Mealie", + slug="mealie", + uuid="1234", + ), + context={"source": SOURCE_HASSIO}, + ) + assert result + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (MealieConnectionError, "cannot_connect"), + (MealieAuthenticationError, "invalid_auth"), + (Exception, "unknown"), + ], +) +async def test_hassio_connection_error( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_setup_entry: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test flow errors.""" + mock_mealie_client.get_user_info.side_effect = exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=HassioServiceInfo( + config={"addon": "Mealie", "host": "http://test", "port": 9090}, + name="mealie", + slug="mealie", + uuid="1234", + ), + context={"source": SOURCE_HASSIO}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "hassio_confirm" + assert result["description_placeholders"] == {"addon": "Mealie"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_API_TOKEN: "token"} + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + mock_mealie_client.get_user_info.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_API_TOKEN: "token"} + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY