"""Config flow for Control4 integration.""" import logging from typing import Any, override from aiohttp.client_exceptions import ClientError from pyControl4.account import C4Account from pyControl4.director import C4Director from pyControl4.error_handling import BadCredentials, NotFound, Unauthorized import voluptuous as vol from homeassistant.config_entries import ( ConfigFlow, ConfigFlowResult, OptionsFlowWithReload, ) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME, ) from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.device_registry import format_mac from . import Control4ConfigEntry from .const import ( CONF_CONTROLLER_UNIQUE_ID, DEFAULT_SCAN_INTERVAL, DOMAIN, MIN_SCAN_INTERVAL, ) _LOGGER = logging.getLogger(__name__) DATA_SCHEMA = vol.Schema( { vol.Required(CONF_HOST): str, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, } ) class Control4ConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Control4.""" VERSION = 1 async def _async_try_connect( self, user_input: dict[str, Any] ) -> tuple[dict[str, str], dict[str, Any] | None, dict[str, str]]: """Try to connect to Control4 and return errors, data, and placeholders.""" errors: dict[str, str] = {} description_placeholders: dict[str, str] = {} data: dict[str, Any] | None = None host = user_input[CONF_HOST] username = user_input[CONF_USERNAME] password = user_input[CONF_PASSWORD] # Step 1: Authenticate with Control4 cloud API account_session = aiohttp_client.async_get_clientsession(self.hass) account = C4Account(username, password, account_session) try: await account.get_account_bearer_token() account_controllers = await account.get_account_controllers() controller_unique_id = account_controllers["controllerCommonName"] director_bearer_token = ( await account.get_director_bearer_token(controller_unique_id) )["token"] except BadCredentials, Unauthorized: errors["base"] = "invalid_auth" return errors, data, description_placeholders except NotFound: errors["base"] = "controller_not_found" return errors, data, description_placeholders except Exception: _LOGGER.exception( "Unexpected exception during Control4 account authentication" ) errors["base"] = "unknown" return errors, data, description_placeholders # Step 2: Connect to local Control4 Director director_session = aiohttp_client.async_get_clientsession( self.hass, verify_ssl=False ) director = C4Director(host, director_bearer_token, director_session) try: await director.get_all_item_info() except Unauthorized: errors["base"] = "director_auth_failed" return errors, data, description_placeholders except ClientError, TimeoutError: errors["base"] = "cannot_connect" description_placeholders["host"] = host return errors, data, description_placeholders except Exception: _LOGGER.exception( "Unexpected exception during Control4 director connection" ) errors["base"] = "unknown" return errors, data, description_placeholders # Success - return the data needed for entry creation data = { CONF_HOST: host, CONF_USERNAME: username, CONF_PASSWORD: password, CONF_CONTROLLER_UNIQUE_ID: controller_unique_id, } return errors, data, description_placeholders @override async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} description_placeholders: dict[str, str] = {} if user_input is not None: errors, data, description_placeholders = await self._async_try_connect( user_input ) if not errors and data is not None: controller_unique_id = data[CONF_CONTROLLER_UNIQUE_ID] mac = (controller_unique_id.split("_", 3))[2] formatted_mac = format_mac(mac) await self.async_set_unique_id(formatted_mac) self._abort_if_unique_id_configured() return self.async_create_entry( title=controller_unique_id, data=data, ) return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors, description_placeholders=description_placeholders, ) @staticmethod @callback @override def async_get_options_flow( config_entry: Control4ConfigEntry, ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler() class OptionsFlowHandler(OptionsFlowWithReload): """Handle a option flow for Control4.""" async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle options flow.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) data_schema = vol.Schema( { # Polling interval is user-configurable, which is no longer allowed # pylint: disable-next=home-assistant-config-flow-polling-field vol.Optional( CONF_SCAN_INTERVAL, default=self.config_entry.options.get( CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ), ): vol.All(cv.positive_int, vol.Clamp(min=MIN_SCAN_INTERVAL)), } ) return self.async_show_form(step_id="init", data_schema=data_schema)