Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Bond integration."""
2 
3 from __future__ import annotations
4 
5 import contextlib
6 from http import HTTPStatus
7 import logging
8 from typing import Any
9 
10 from aiohttp import ClientConnectionError, ClientResponseError
11 from bond_async import Bond
12 import voluptuous as vol
13 
14 from homeassistant.components import zeroconf
15 from homeassistant.config_entries import ConfigEntryState, ConfigFlow, ConfigFlowResult
16 from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME
17 from homeassistant.core import HomeAssistant
18 from homeassistant.exceptions import HomeAssistantError
19 from homeassistant.helpers.aiohttp_client import async_get_clientsession
20 
21 from .const import DOMAIN
22 from .utils import BondHub
23 
24 _LOGGER = logging.getLogger(__name__)
25 
26 
27 USER_SCHEMA = vol.Schema(
28  {vol.Required(CONF_HOST): str, vol.Required(CONF_ACCESS_TOKEN): str}
29 )
30 DISCOVERY_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str})
31 TOKEN_SCHEMA = vol.Schema({})
32 
33 
34 async def async_get_token(hass: HomeAssistant, host: str) -> str | None:
35  """Try to fetch the token from the bond device."""
36  bond = Bond(host, "", session=async_get_clientsession(hass))
37  response: dict[str, str] = {}
38  with contextlib.suppress(ClientConnectionError):
39  response = await bond.token()
40  return response.get("token")
41 
42 
43 async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[str, str]:
44  """Validate the user input allows us to connect."""
45 
46  bond = Bond(
47  data[CONF_HOST], data[CONF_ACCESS_TOKEN], session=async_get_clientsession(hass)
48  )
49  try:
50  hub = BondHub(bond, data[CONF_HOST])
51  await hub.setup(max_devices=1)
52  except ClientConnectionError as error:
53  raise InputValidationError("cannot_connect") from error
54  except ClientResponseError as error:
55  if error.status == HTTPStatus.UNAUTHORIZED:
56  raise InputValidationError("invalid_auth") from error
57  raise InputValidationError("unknown") from error
58  except Exception as error:
59  _LOGGER.exception("Unexpected exception")
60  raise InputValidationError("unknown") from error
61 
62  # Return unique ID from the hub to be stored in the config entry.
63  if not hub.bond_id:
64  raise InputValidationError("old_firmware")
65 
66  return hub.bond_id, hub.name
67 
68 
69 class BondConfigFlow(ConfigFlow, domain=DOMAIN):
70  """Handle a config flow for Bond."""
71 
72  VERSION = 1
73 
74  def __init__(self) -> None:
75  """Initialize config flow."""
76  self._discovered_discovered: dict[str, str] = {}
77 
78  async def _async_try_automatic_configure(self) -> None:
79  """Try to auto configure the device.
80 
81  Failure is acceptable here since the device may have been
82  online longer then the allowed setup period, and we will
83  instead ask them to manually enter the token.
84  """
85  host = self._discovered_discovered[CONF_HOST]
86  try:
87  if not (token := await async_get_token(self.hass, host)):
88  return
89  except TimeoutError:
90  return
91 
92  self._discovered_discovered[CONF_ACCESS_TOKEN] = token
93  try:
94  _, hub_name = await _validate_input(self.hass, self._discovered_discovered)
95  except InputValidationError:
96  return
97  self._discovered_discovered[CONF_NAME] = hub_name
98 
100  self, discovery_info: zeroconf.ZeroconfServiceInfo
101  ) -> ConfigFlowResult:
102  """Handle a flow initialized by zeroconf discovery."""
103  name: str = discovery_info.name
104  host: str = discovery_info.host
105  bond_id = name.partition(".")[0]
106  await self.async_set_unique_idasync_set_unique_id(bond_id)
107  for entry in self._async_current_entries_async_current_entries():
108  if entry.unique_id != bond_id:
109  continue
110  updates = {CONF_HOST: host}
111  if entry.state == ConfigEntryState.SETUP_ERROR and (
112  token := await async_get_token(self.hass, host)
113  ):
114  updates[CONF_ACCESS_TOKEN] = token
115  return self.async_update_reload_and_abortasync_update_reload_and_abort(
116  entry,
117  data={**entry.data, **updates},
118  reason="already_configured",
119  reload_even_if_entry_is_unchanged=False,
120  )
121 
122  self._discovered_discovered = {CONF_HOST: host, CONF_NAME: bond_id}
123  await self._async_try_automatic_configure_async_try_automatic_configure()
124 
125  self.context.update(
126  {
127  "title_placeholders": {
128  CONF_HOST: self._discovered_discovered[CONF_HOST],
129  CONF_NAME: self._discovered_discovered[CONF_NAME],
130  }
131  }
132  )
133 
134  return await self.async_step_confirmasync_step_confirm()
135 
137  self, user_input: dict[str, Any] | None = None
138  ) -> ConfigFlowResult:
139  """Handle confirmation flow for discovered bond hub."""
140  errors = {}
141  if user_input is not None:
142  if CONF_ACCESS_TOKEN in self._discovered_discovered:
143  return self.async_create_entryasync_create_entryasync_create_entry(
144  title=self._discovered_discovered[CONF_NAME],
145  data={
146  CONF_ACCESS_TOKEN: self._discovered_discovered[CONF_ACCESS_TOKEN],
147  CONF_HOST: self._discovered_discovered[CONF_HOST],
148  },
149  )
150 
151  data = {
152  CONF_ACCESS_TOKEN: user_input[CONF_ACCESS_TOKEN],
153  CONF_HOST: self._discovered_discovered[CONF_HOST],
154  }
155  try:
156  _, hub_name = await _validate_input(self.hass, data)
157  except InputValidationError as error:
158  errors["base"] = error.base
159  else:
160  return self.async_create_entryasync_create_entryasync_create_entry(
161  title=hub_name,
162  data=data,
163  )
164 
165  if CONF_ACCESS_TOKEN in self._discovered_discovered:
166  data_schema = TOKEN_SCHEMA
167  else:
168  data_schema = DISCOVERY_SCHEMA
169 
170  return self.async_show_formasync_show_formasync_show_form(
171  step_id="confirm",
172  data_schema=data_schema,
173  errors=errors,
174  description_placeholders=self._discovered_discovered,
175  )
176 
177  async def async_step_user(
178  self, user_input: dict[str, Any] | None = None
179  ) -> ConfigFlowResult:
180  """Handle a flow initialized by the user."""
181  errors = {}
182  if user_input is not None:
183  try:
184  bond_id, hub_name = await _validate_input(self.hass, user_input)
185  except InputValidationError as error:
186  errors["base"] = error.base
187  else:
188  await self.async_set_unique_idasync_set_unique_id(bond_id)
189  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
190  return self.async_create_entryasync_create_entryasync_create_entry(title=hub_name, data=user_input)
191 
192  return self.async_show_formasync_show_formasync_show_form(
193  step_id="user", data_schema=USER_SCHEMA, errors=errors
194  )
195 
196 
198  """Error to indicate we cannot proceed due to invalid input."""
199 
200  def __init__(self, base: str) -> None:
201  """Initialize with error base."""
202  super().__init__()
203  self.basebase = base
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:101
ConfigFlowResult async_step_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:138
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:179
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)
list[ConfigEntry] _async_current_entries(self, bool|None include_ignore=None)
ConfigFlowResult async_update_reload_and_abort(self, ConfigEntry entry, *str|None|UndefinedType unique_id=UNDEFINED, str|UndefinedType title=UNDEFINED, Mapping[str, Any]|UndefinedType data=UNDEFINED, Mapping[str, Any]|UndefinedType data_updates=UNDEFINED, Mapping[str, Any]|UndefinedType options=UNDEFINED, str|UndefinedType reason=UNDEFINED, bool reload_even_if_entry_is_unchanged=True)
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)
_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)
str|None async_get_token(HomeAssistant hass, str host)
Definition: config_flow.py:34
tuple[str, str] _validate_input(HomeAssistant hass, dict[str, Any] data)
Definition: config_flow.py:43
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)