diff --git a/homeassistant/components/wyoming/config_flow.py b/homeassistant/components/wyoming/config_flow.py index 2788f30aeef..ee7e1d574ac 100644 --- a/homeassistant/components/wyoming/config_flow.py +++ b/homeassistant/components/wyoming/config_flow.py @@ -2,10 +2,12 @@ from __future__ import annotations from typing import Any +from urllib.parse import urlparse import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.data_entry_flow import FlowResult @@ -25,6 +27,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + _hassio_discovery: HassioServiceInfo + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -54,3 +58,36 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): name = asr_installed[0].name return self.async_create_entry(title=name, data=user_input) + + async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: + """Handle Supervisor add-on discovery.""" + await self.async_set_unique_id(discovery_info.slug) + self._abort_if_unique_id_configured() + + self._hassio_discovery = discovery_info + return await self.async_step_hassio_confirm() + + async def async_step_hassio_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm Supervisor discovery.""" + errors: dict[str, str] = {} + + if user_input is not None: + uri = urlparse(self._hassio_discovery.config["uri"]) + if service := await WyomingService.create(uri.hostname, uri.port): + if not any(asr for asr in service.info.asr if asr.installed): + return self.async_abort(reason="no_services") + + return self.async_create_entry( + title=self._hassio_discovery.name, + data={CONF_HOST: uri.hostname, CONF_PORT: uri.port}, + ) + + errors = {"base": "cannot_connect"} + + return self.async_show_form( + step_id="hassio_confirm", + description_placeholders={"addon": self._hassio_discovery.name}, + errors=errors, + ) diff --git a/homeassistant/components/wyoming/strings.json b/homeassistant/components/wyoming/strings.json index 76f6b837b80..20d73d8dc13 100644 --- a/homeassistant/components/wyoming/strings.json +++ b/homeassistant/components/wyoming/strings.json @@ -6,12 +6,16 @@ "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]" } + }, + "hassio_confirm": { + "description": "Do you want to configure Home Assistant to connect to the Wyoming service provided by the add-on: {addon}?" } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", "no_services": "No services found at endpoint" } } diff --git a/tests/components/wyoming/snapshots/test_config_flow.ambr b/tests/components/wyoming/snapshots/test_config_flow.ambr new file mode 100644 index 00000000000..d8f08bb6087 --- /dev/null +++ b/tests/components/wyoming/snapshots/test_config_flow.ambr @@ -0,0 +1,39 @@ +# serializer version: 1 +# name: test_hassio_addon_discovery + FlowResultSnapshot({ + 'context': dict({ + 'source': 'hassio', + 'unique_id': 'mock_piper', + }), + 'data': dict({ + 'host': 'mock-piper', + 'port': 10200, + }), + 'description': None, + 'description_placeholders': None, + 'flow_id': , + 'handler': 'wyoming', + 'options': dict({ + }), + 'result': ConfigEntrySnapshot({ + 'data': dict({ + 'host': 'mock-piper', + 'port': 10200, + }), + 'disabled_by': None, + 'domain': 'wyoming', + 'entry_id': , + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'hassio', + 'title': 'Piper', + 'unique_id': 'mock_piper', + 'version': 1, + }), + 'title': 'Piper', + 'type': , + 'version': 1, + }) +# --- diff --git a/tests/components/wyoming/test_config_flow.py b/tests/components/wyoming/test_config_flow.py index 8a0bf4955e7..9f9b123a411 100644 --- a/tests/components/wyoming/test_config_flow.py +++ b/tests/components/wyoming/test_config_flow.py @@ -2,14 +2,27 @@ from unittest.mock import AsyncMock, patch import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant import config_entries +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.wyoming.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from . import EMPTY_INFO, STT_INFO +from tests.common import MockConfigEntry + +ADDON_DISCOVERY = HassioServiceInfo( + config={ + "addon": "Piper", + "uri": "tcp://mock-piper:10200", + }, + name="Piper", + slug="mock_piper", +) + pytestmark = pytest.mark.usefixtures("mock_setup_entry") @@ -85,3 +98,85 @@ async def test_no_supported_services(hass: HomeAssistant) -> None: assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "no_services" + + +async def test_hassio_addon_discovery( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + snapshot: SnapshotAssertion, +) -> None: + """Test config flow initiated by Supervisor.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=ADDON_DISCOVERY, + context={"source": config_entries.SOURCE_HASSIO}, + ) + + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "hassio_confirm" + assert result.get("description_placeholders") == {"addon": "Piper"} + + with patch( + "homeassistant.components.wyoming.data.load_wyoming_info", + return_value=STT_INFO, + ) as mock_wyoming: + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2 == snapshot + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_wyoming.mock_calls) == 1 + + +async def test_hassio_addon_already_configured(hass: HomeAssistant) -> None: + """Test we abort discovery if the add-on is already configured.""" + MockConfigEntry( + domain=DOMAIN, + data={"host": "mock-piper", "port": "10200"}, + unique_id="mock_piper", + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=ADDON_DISCOVERY, + context={"source": config_entries.SOURCE_HASSIO}, + ) + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "already_configured" + + +async def test_hassio_addon_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=ADDON_DISCOVERY, + context={"source": config_entries.SOURCE_HASSIO}, + ) + + with patch( + "homeassistant.components.wyoming.data.load_wyoming_info", + return_value=None, + ): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("errors") == {"base": "cannot_connect"} + + +async def test_hassio_addon_no_supported_services(hass: HomeAssistant) -> None: + """Test we handle no supported services error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=ADDON_DISCOVERY, + context={"source": config_entries.SOURCE_HASSIO}, + ) + + with patch( + "homeassistant.components.wyoming.data.load_wyoming_info", + return_value=EMPTY_INFO, + ): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result2.get("type") == FlowResultType.ABORT + assert result2.get("reason") == "no_services"