"""Config flow for UPB PIM integration.""" import asyncio from contextlib import suppress import logging from urllib.parse import urlparse import async_timeout import upb_lib import voluptuous as vol from homeassistant import config_entries, exceptions from homeassistant.const import CONF_ADDRESS, CONF_FILE_PATH, CONF_HOST, CONF_PROTOCOL from .const import DOMAIN _LOGGER = logging.getLogger(__name__) PROTOCOL_MAP = {"TCP": "tcp://", "Serial port": "serial://"} DATA_SCHEMA = vol.Schema( { vol.Required(CONF_PROTOCOL, default="Serial port"): vol.In( ["TCP", "Serial port"] ), vol.Required(CONF_ADDRESS): str, vol.Required(CONF_FILE_PATH, default=""): str, } ) VALIDATE_TIMEOUT = 15 async def _validate_input(data): """Validate the user input allows us to connect.""" def _connected_callback(): connected_event.set() connected_event = asyncio.Event() file_path = data.get(CONF_FILE_PATH) url = _make_url_from_data(data) upb = upb_lib.UpbPim({"url": url, "UPStartExportFile": file_path}) if not upb.config_ok: _LOGGER.error("Missing or invalid UPB file: %s", file_path) raise InvalidUpbFile upb.connect(_connected_callback) with suppress(asyncio.TimeoutError), async_timeout.timeout(VALIDATE_TIMEOUT): await connected_event.wait() upb.disconnect() if not connected_event.is_set(): _LOGGER.error( "Timed out after %d seconds trying to connect with UPB PIM at %s", VALIDATE_TIMEOUT, url, ) raise CannotConnect # Return info that you want to store in the config entry. return (upb.network_id, {"title": "UPB", CONF_HOST: url, CONF_FILE_PATH: file_path}) def _make_url_from_data(data): if host := data.get(CONF_HOST): return host protocol = PROTOCOL_MAP[data[CONF_PROTOCOL]] address = data[CONF_ADDRESS] return f"{protocol}{address}" class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for UPB PIM.""" VERSION = 1 def __init__(self): """Initialize the UPB config flow.""" self.importing = False async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} if user_input is not None: try: if self._url_already_configured(_make_url_from_data(user_input)): return self.async_abort(reason="already_configured") network_id, info = await _validate_input(user_input) except CannotConnect: errors["base"] = "cannot_connect" except InvalidUpbFile: errors["base"] = "invalid_upb_file" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" if "base" not in errors: await self.async_set_unique_id(network_id) self._abort_if_unique_id_configured() if self.importing: return self.async_create_entry(title=info["title"], data=user_input) return self.async_create_entry( title=info["title"], data={ CONF_HOST: info[CONF_HOST], CONF_FILE_PATH: user_input[CONF_FILE_PATH], }, ) return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors ) async def async_step_import(self, user_input): """Handle import.""" self.importing = True return await self.async_step_user(user_input) def _url_already_configured(self, url): """See if we already have a UPB PIM matching user input configured.""" existing_hosts = { urlparse(entry.data[CONF_HOST]).hostname for entry in self._async_current_entries() } return urlparse(url).hostname in existing_hosts class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" class InvalidUpbFile(exceptions.HomeAssistantError): """Error to indicate there is invalid or missing UPB config file."""