Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Control4 integration."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import TYPE_CHECKING, Any
7 
8 from aiohttp.client_exceptions import ClientError
9 from pyControl4.account import C4Account
10 from pyControl4.director import C4Director
11 from pyControl4.error_handling import NotFound, Unauthorized
12 import voluptuous as vol
13 
14 from homeassistant.config_entries import (
15  ConfigEntry,
16  ConfigFlow,
17  ConfigFlowResult,
18  OptionsFlow,
19 )
20 from homeassistant.const import (
21  CONF_HOST,
22  CONF_PASSWORD,
23  CONF_SCAN_INTERVAL,
24  CONF_USERNAME,
25 )
26 from homeassistant.core import HomeAssistant, callback
27 from homeassistant.exceptions import HomeAssistantError
28 from homeassistant.helpers import aiohttp_client, config_validation as cv
29 from homeassistant.helpers.device_registry import format_mac
30 
31 from .const import (
32  CONF_CONTROLLER_UNIQUE_ID,
33  DEFAULT_SCAN_INTERVAL,
34  DOMAIN,
35  MIN_SCAN_INTERVAL,
36 )
37 
38 _LOGGER = logging.getLogger(__name__)
39 
40 DATA_SCHEMA = vol.Schema(
41  {
42  vol.Required(CONF_HOST): str,
43  vol.Required(CONF_USERNAME): str,
44  vol.Required(CONF_PASSWORD): str,
45  }
46 )
47 
48 
50  """Validates that config details can be used to authenticate and communicate with Control4."""
51 
52  def __init__(
53  self, host: str, username: str, password: str, hass: HomeAssistant
54  ) -> None:
55  """Initialize."""
56  self.hosthost = host
57  self.usernameusername = username
58  self.passwordpassword = password
59  self.controller_unique_idcontroller_unique_id = None
60  self.director_bearer_tokendirector_bearer_token = None
61  self.hasshass = hass
62 
63  async def authenticate(self) -> bool:
64  """Test if we can authenticate with the Control4 account API."""
65  try:
66  account_session = aiohttp_client.async_get_clientsession(self.hasshass)
67  account = C4Account(self.usernameusername, self.passwordpassword, account_session)
68  # Authenticate with Control4 account
69  await account.getAccountBearerToken()
70 
71  # Get controller name
72  account_controllers = await account.getAccountControllers()
73  self.controller_unique_idcontroller_unique_id = account_controllers["controllerCommonName"]
74 
75  # Get bearer token to communicate with controller locally
76  self.director_bearer_tokendirector_bearer_token = (
77  await account.getDirectorBearerToken(self.controller_unique_idcontroller_unique_id)
78  )["token"]
79  except (Unauthorized, NotFound):
80  return False
81  return True
82 
83  async def connect_to_director(self) -> bool:
84  """Test if we can connect to the local Control4 Director."""
85  try:
86  director_session = aiohttp_client.async_get_clientsession(
87  self.hasshass, verify_ssl=False
88  )
89  director = C4Director(
90  self.hosthost, self.director_bearer_tokendirector_bearer_token, director_session
91  )
92  await director.getAllItemInfo()
93  except (Unauthorized, ClientError, TimeoutError):
94  _LOGGER.error("Failed to connect to the Control4 controller")
95  return False
96  return True
97 
98 
99 class Control4ConfigFlow(ConfigFlow, domain=DOMAIN):
100  """Handle a config flow for Control4."""
101 
102  VERSION = 1
103 
104  async def async_step_user(
105  self, user_input: dict[str, Any] | None = None
106  ) -> ConfigFlowResult:
107  """Handle the initial step."""
108  errors = {}
109  if user_input is not None:
110  hub = Control4Validator(
111  user_input[CONF_HOST],
112  user_input[CONF_USERNAME],
113  user_input[CONF_PASSWORD],
114  self.hass,
115  )
116  try:
117  if not await hub.authenticate():
118  raise InvalidAuth # noqa: TRY301
119  if not await hub.connect_to_director():
120  raise CannotConnect # noqa: TRY301
121  except InvalidAuth:
122  errors["base"] = "invalid_auth"
123  except CannotConnect:
124  errors["base"] = "cannot_connect"
125  except Exception:
126  _LOGGER.exception("Unexpected exception")
127  errors["base"] = "unknown"
128 
129  if not errors:
130  controller_unique_id = hub.controller_unique_id
131  if TYPE_CHECKING:
132  assert hub.controller_unique_id
133  mac = (controller_unique_id.split("_", 3))[2]
134  formatted_mac = format_mac(mac)
135  await self.async_set_unique_idasync_set_unique_id(formatted_mac)
136  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
137  return self.async_create_entryasync_create_entryasync_create_entry(
138  title=controller_unique_id,
139  data={
140  CONF_HOST: user_input[CONF_HOST],
141  CONF_USERNAME: user_input[CONF_USERNAME],
142  CONF_PASSWORD: user_input[CONF_PASSWORD],
143  CONF_CONTROLLER_UNIQUE_ID: controller_unique_id,
144  },
145  )
146 
147  return self.async_show_formasync_show_formasync_show_form(
148  step_id="user", data_schema=DATA_SCHEMA, errors=errors
149  )
150 
151  @staticmethod
152  @callback
154  config_entry: ConfigEntry,
155  ) -> OptionsFlowHandler:
156  """Get the options flow for this handler."""
157  return OptionsFlowHandler()
158 
159 
161  """Handle a option flow for Control4."""
162 
163  async def async_step_init(
164  self, user_input: dict[str, Any] | None = None
165  ) -> ConfigFlowResult:
166  """Handle options flow."""
167  if user_input is not None:
168  return self.async_create_entryasync_create_entry(title="", data=user_input)
169 
170  data_schema = vol.Schema(
171  {
172  vol.Optional(
173  CONF_SCAN_INTERVAL,
174  default=self.config_entryconfig_entryconfig_entry.options.get(
175  CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
176  ),
177  ): vol.All(cv.positive_int, vol.Clamp(min=MIN_SCAN_INTERVAL)),
178  }
179  )
180  return self.async_show_formasync_show_form(step_id="init", data_schema=data_schema)
181 
182 
184  """Error to indicate we cannot connect."""
185 
186 
187 class InvalidAuth(HomeAssistantError):
188  """Error to indicate there is invalid auth."""
OptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:155
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:106
None __init__(self, str host, str username, str password, HomeAssistant hass)
Definition: config_flow.py:54
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:165
None _abort_if_unique_id_configured(self, dict[str, Any]|None updates=None, bool reload_on_update=True, *str error="already_configured")
ConfigEntry|None async_set_unique_id(self, str|None unique_id=None, *bool raise_on_progress=True)
ConfigFlowResult async_create_entry(self, *str title, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None, Mapping[str, Any]|None options=None)
ConfigFlowResult async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
None config_entry(self, ConfigEntry value)
_FlowResultT async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
_FlowResultT async_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)