From d8df89a34e79aa42b08545133e7337ae1727ea33 Mon Sep 17 00:00:00 2001 From: mib1185 Date: Mon, 16 Jun 2025 21:44:28 +0000 Subject: [PATCH] add support for discovery by add-ons --- .../components/immich/config_flow.py | 65 +++++++++ homeassistant/components/immich/strings.json | 11 ++ tests/components/immich/const.py | 8 ++ tests/components/immich/test_config_flow.py | 125 +++++++++++++++++- 4 files changed, 206 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/immich/config_flow.py b/homeassistant/components/immich/config_flow.py index 69fae3ff1eb..f17e9a00958 100644 --- a/homeassistant/components/immich/config_flow.py +++ b/homeassistant/components/immich/config_flow.py @@ -30,6 +30,7 @@ from homeassistant.helpers.selector import ( TextSelectorConfig, TextSelectorType, ) +from homeassistant.helpers.service_info.hassio import HassioServiceInfo from .const import DEFAULT_VERIFY_SSL, DOMAIN @@ -81,6 +82,7 @@ class ImmichConfigFlow(ConfigFlow, domain=DOMAIN): _name: str _current_data: Mapping[str, Any] + _hassio_discovery: dict[str, Any] | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -172,3 +174,66 @@ class ImmichConfigFlow(ConfigFlow, domain=DOMAIN): description_placeholders={"name": self._name}, errors=errors, ) + + async def async_step_hassio( + self, discovery_info: HassioServiceInfo + ) -> ConfigFlowResult: + """Handle the discovery step via hassio.""" + 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.""" + assert self._hassio_discovery + host = self._hassio_discovery[CONF_HOST] + port = self._hassio_discovery[CONF_PORT] + ssl = self._hassio_discovery[CONF_SSL] + addon = self._hassio_discovery["addon"] + + errors: dict[str, str] = {} + if user_input is not None: + try: + my_user_info = await check_user_info( + self.hass, + host, + port, + ssl, + user_input[CONF_VERIFY_SSL], + user_input[CONF_API_KEY], + ) + except ImmichUnauthorizedError: + errors["base"] = "invalid_auth" + except CONNECT_ERRORS: + errors["base"] = "cannot_connect" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(my_user_info.user_id) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=my_user_info.name, + data={ + CONF_HOST: host, + CONF_PORT: port, + CONF_SSL: ssl, + CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL], + CONF_API_KEY: user_input[CONF_API_KEY], + }, + ) + + return self.async_show_form( + step_id="hassio_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY): TextSelector( + config=TextSelectorConfig(type=TextSelectorType.PASSWORD) + ), + vol.Required(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool, + } + ), + description_placeholders={"addon": addon}, + errors=errors, + ) diff --git a/homeassistant/components/immich/strings.json b/homeassistant/components/immich/strings.json index 875eb79f50b..3fa2a025b6c 100644 --- a/homeassistant/components/immich/strings.json +++ b/homeassistant/components/immich/strings.json @@ -26,6 +26,17 @@ "data_description": { "api_key": "[%key:component::immich::common::data_desc_api_key%]" } + }, + "hassio_confirm": { + "description": "Setup connection to add-on {addon}", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + }, + "data_description": { + "api_key": "[%key:component::immich::common::data_desc_api_key%]", + "verify_ssl": "[%key:component::immich::common::data_desc_ssl_verify%]" + } } }, "error": { diff --git a/tests/components/immich/const.py b/tests/components/immich/const.py index 97721bc7dbc..d1e213571b6 100644 --- a/tests/components/immich/const.py +++ b/tests/components/immich/const.py @@ -25,6 +25,14 @@ MOCK_CONFIG_ENTRY_DATA = { CONF_VERIFY_SSL: False, } +MOCK_CONFIG_ENTRY_DATA_HASSIO = { + CONF_HOST: "172.16.10.1", + CONF_API_KEY: "abcdef0123456789", + CONF_PORT: 8080, + CONF_SSL: False, + CONF_VERIFY_SSL: False, +} + ALBUM_DATA = { "id": "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", "albumName": "My Album", diff --git a/tests/components/immich/test_config_flow.py b/tests/components/immich/test_config_flow.py index e26cb4df5a1..1660e1e8deb 100644 --- a/tests/components/immich/test_config_flow.py +++ b/tests/components/immich/test_config_flow.py @@ -7,12 +7,13 @@ from aioimmich.exceptions import ImmichUnauthorizedError import pytest from homeassistant.components.immich.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_API_KEY, CONF_URL +from homeassistant.config_entries import SOURCE_HASSIO, SOURCE_USER +from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.hassio import HassioServiceInfo -from .const import MOCK_CONFIG_ENTRY_DATA, MOCK_USER_DATA +from .const import MOCK_CONFIG_ENTRY_DATA, MOCK_CONFIG_ENTRY_DATA_HASSIO, MOCK_USER_DATA from tests.common import MockConfigEntry @@ -242,3 +243,121 @@ async def test_reauth_flow_mismatch( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unique_id_mismatch" + + +async def test_hassio_flow( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_immich: Mock +) -> None: + """Test discovery via hassio.""" + + test_data = HassioServiceInfo( + config={ + "host": "172.16.10.1", + "port": 8080, + "ssl": False, + "addon": "IMMICH", + }, + name="IMMICH", + slug="immich", + uuid="1234", + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_HASSIO}, + data=test_data, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "hassio_confirm" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: MOCK_USER_DATA[CONF_API_KEY], + CONF_VERIFY_SSL: MOCK_USER_DATA[CONF_VERIFY_SSL], + }, + ) + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "user" + assert result2["data"] == MOCK_CONFIG_ENTRY_DATA_HASSIO + assert result2["result"].unique_id == "e7ef5713-9dab-4bd4-b899-715b0ca4379e" + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + ( + ImmichUnauthorizedError( + { + "message": "Invalid API key", + "error": "Unauthenticated", + "statusCode": 401, + "correlationId": "abcdefg", + } + ), + "invalid_auth", + ), + (ClientError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_hassio_flow_error_handling( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_immich: Mock, + exception: Exception, + error: str, +) -> None: + """Test error handling during discovery via hassio.""" + + test_data = HassioServiceInfo( + config={ + "host": "172.16.10.1", + "port": 8080, + "ssl": False, + "addon": "IMMICH", + }, + name="IMMICH", + slug="immich", + uuid="1234", + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_HASSIO}, + data=test_data, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "hassio_confirm" + + mock_immich.users.async_get_my_user.side_effect = exception + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: MOCK_USER_DATA[CONF_API_KEY], + CONF_VERIFY_SSL: MOCK_USER_DATA[CONF_VERIFY_SSL], + }, + ) + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "hassio_confirm" + assert result2["errors"] == {"base": error} + + mock_immich.users.async_get_my_user.side_effect = None + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: MOCK_USER_DATA[CONF_API_KEY], + CONF_VERIFY_SSL: MOCK_USER_DATA[CONF_VERIFY_SSL], + }, + ) + + assert result3["type"] is FlowResultType.CREATE_ENTRY + assert result3["title"] == "user" + assert result3["data"] == MOCK_CONFIG_ENTRY_DATA_HASSIO + assert result3["result"].unique_id == "e7ef5713-9dab-4bd4-b899-715b0ca4379e"