"""Config flow for Flow-it integration.""" import logging from typing import TYPE_CHECKING, Any, override from flow_it_api.client import FlowItVMCMachine from flow_it_api.exceptions import FlowItAuthError, FlowItConnectionError import voluptuous as vol from yarl import URL from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.selector import ( TextSelector, TextSelectorConfig, TextSelectorType, ) from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from .const import DEFAULT_USERNAME, DOMAIN _LOGGER = logging.getLogger(__name__) async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: """Validate the user input allows us to connect.""" vmc = FlowItVMCMachine( data[CONF_HOST], data[CONF_PASSWORD], data[CONF_USERNAME], session=get_async_client(hass), ) info = await vmc.get_info() await vmc.refresh_state() if TYPE_CHECKING: assert vmc.state is not None return { "title": info.hostname, "mac_address": vmc.state.name, } class FlowItConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Flow-it.""" def __init__(self) -> None: """Initialize the config flow.""" self._discovery_info: dict[str, Any] = {} @override async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: host = user_input[CONF_HOST] if not URL(host).scheme: host = str(URL.build(scheme="http", host=host)) user_input[CONF_HOST] = host try: info = await validate_input(self.hass, user_input) except FlowItAuthError: errors["base"] = "invalid_auth" except FlowItConnectionError: errors["base"] = "cannot_connect" except Exception: _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: await self.async_set_unique_id(info["mac_address"]) self._abort_if_unique_id_configured(updates=user_input) return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_form( step_id="user", data_schema=vol.Schema( { vol.Required(CONF_HOST): str, vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): TextSelector( TextSelectorConfig( type=TextSelectorType.TEXT, autocomplete="username" ) ), vol.Required(CONF_PASSWORD): TextSelector( TextSelectorConfig( type=TextSelectorType.PASSWORD, autocomplete="current-password", ) ), } ), errors=errors, ) async def async_step_zeroconf_confirm( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle a flow initiated by zeroconf.""" errors: dict[str, str] = {} if user_input is not None: host = self._discovery_info[CONF_HOST] if not URL(host).scheme: host = str(URL.build(scheme="http", host=host)) data = { CONF_HOST: host, CONF_USERNAME: user_input[CONF_USERNAME], CONF_PASSWORD: user_input[CONF_PASSWORD], } try: info = await validate_input(self.hass, data) except FlowItAuthError: errors["base"] = "invalid_auth" except FlowItConnectionError: errors["base"] = "cannot_connect" except Exception: _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: await self.async_set_unique_id(info["mac_address"]) self._abort_if_unique_id_configured(updates=data) return self.async_create_entry( title=info["title"], data=data, ) data_schema = vol.Schema( { vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): TextSelector( TextSelectorConfig( type=TextSelectorType.TEXT, autocomplete="username" ) ), vol.Required(CONF_PASSWORD): TextSelector( TextSelectorConfig( type=TextSelectorType.PASSWORD, autocomplete="current-password" ) ), } ) return self.async_show_form( step_id="zeroconf_confirm", data_schema=data_schema, errors=errors, description_placeholders={ "name": self._discovery_info.get( "friendly_name", self._discovery_info[CONF_HOST].removesuffix(".local"), ) }, ) @override async def async_step_zeroconf( self, discovery_info: ZeroconfServiceInfo ) -> ConfigFlowResult: """Handle zeroconf discovery.""" host = discovery_info.host hostname = discovery_info.hostname.rstrip(".") friendly_name = discovery_info.name.removesuffix("._tbk_vmc._tcp.local.") self._discovery_info = { CONF_HOST: hostname, "friendly_name": friendly_name, } self._async_abort_entries_match({CONF_HOST: host}) self._async_abort_entries_match({CONF_HOST: hostname}) self._async_abort_entries_match({CONF_HOST: f"http://{host}"}) self._async_abort_entries_match({CONF_HOST: f"http://{hostname}"}) self.context.update({"title_placeholders": {"name": friendly_name}}) return await self.async_step_zeroconf_confirm()