Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Awair."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 from typing import Any, Self, cast
7 
8 from aiohttp.client_exceptions import ClientError
9 from python_awair import Awair, AwairLocal, AwairLocalDevice
10 from python_awair.exceptions import AuthError, AwairError
11 from python_awair.user import AwairUser
12 import voluptuous as vol
13 
14 from homeassistant.components import onboarding, zeroconf
15 from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow, ConfigFlowResult
16 from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DEVICE, CONF_HOST
17 from homeassistant.core import callback
18 from homeassistant.helpers.aiohttp_client import async_get_clientsession
19 
20 from .const import DOMAIN, LOGGER
21 
22 
23 class AwairFlowHandler(ConfigFlow, domain=DOMAIN):
24  """Config flow for Awair."""
25 
26  VERSION = 1
27 
28  _device: AwairLocalDevice
29  host: str
30 
32  self, discovery_info: zeroconf.ZeroconfServiceInfo
33  ) -> ConfigFlowResult:
34  """Handle zeroconf discovery."""
35 
36  self.hosthost = discovery_info.host
37  LOGGER.debug("Discovered device: %s", self.hosthost)
38 
39  self._device, _ = await self._check_local_connection_check_local_connection(self.hosthost)
40 
41  if self._device is not None:
42  await self.async_set_unique_idasync_set_unique_id(self._device.mac_address)
43  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
44  updates={CONF_HOST: self._device.device_addr},
45  error="already_configured_device",
46  )
47  self.context.update(
48  {
49  "title_placeholders": {
50  "model": self._device.model,
51  "device_id": self._device.device_id,
52  },
53  }
54  )
55  else:
56  return self.async_abortasync_abortasync_abort(reason="unreachable")
57  return await self.async_step_discovery_confirmasync_step_discovery_confirm()
58 
60  self, user_input: dict[str, Any] | None = None
61  ) -> ConfigFlowResult:
62  """Confirm discovery."""
63  if user_input is not None or not onboarding.async_is_onboarded(self.hass):
64  title = f"{self._device.model} ({self._device.device_id})"
65  return self.async_create_entryasync_create_entryasync_create_entry(
66  title=title,
67  data={CONF_HOST: self._device.device_addr},
68  )
69 
70  self._set_confirm_only_set_confirm_only()
71  placeholders = {
72  "model": self._device.model,
73  "device_id": self._device.device_id,
74  }
75  return self.async_show_formasync_show_formasync_show_form(
76  step_id="discovery_confirm",
77  description_placeholders=placeholders,
78  )
79 
80  async def async_step_user(
81  self, user_input: dict[str, str] | None = None
82  ) -> ConfigFlowResult:
83  """Handle a flow initialized by the user."""
84 
85  return self.async_show_menuasync_show_menu(step_id="user", menu_options=["local", "cloud"])
86 
87  async def async_step_cloud(self, user_input: Mapping[str, Any]) -> ConfigFlowResult:
88  """Handle collecting and verifying Awair Cloud API credentials."""
89 
90  errors = {}
91 
92  if user_input is not None:
93  user, error = await self._check_cloud_connection_check_cloud_connection(
94  user_input[CONF_ACCESS_TOKEN]
95  )
96 
97  if user is not None:
98  await self.async_set_unique_idasync_set_unique_id(user.email)
99  self._abort_if_unique_id_configured_abort_if_unique_id_configured(error="already_configured_account")
100 
101  title = user.email
102  return self.async_create_entryasync_create_entryasync_create_entry(title=title, data=user_input)
103 
104  if error and error != "invalid_access_token":
105  return self.async_abortasync_abortasync_abort(reason=error)
106 
107  errors = {CONF_ACCESS_TOKEN: "invalid_access_token"}
108 
109  return self.async_show_formasync_show_formasync_show_form(
110  step_id="cloud",
111  data_schema=vol.Schema({vol.Optional(CONF_ACCESS_TOKEN): str}),
112  description_placeholders={
113  "url": "https://developer.getawair.com/onboard/login"
114  },
115  errors=errors,
116  )
117 
118  @callback
119  def _get_discovered_entries(self) -> dict[str, str]:
120  """Get discovered entries."""
121  entries: dict[str, str] = {}
122 
123  flows = cast(
124  set[Self],
125  self.hass.config_entries.flow._handler_progress_index.get(DOMAIN) or set(), # noqa: SLF001
126  )
127  for flow in flows:
128  if flow.source != SOURCE_ZEROCONF:
129  continue
130  info = flow.context["title_placeholders"]
131  entries[flow.host] = f"{info['model']} ({info['device_id']})"
132  return entries
133 
134  async def async_step_local(
135  self, user_input: Mapping[str, Any] | None = None
136  ) -> ConfigFlowResult:
137  """Show how to enable local API."""
138  if user_input is not None:
139  return await self.async_step_local_pickasync_step_local_pick()
140 
141  return self.async_show_formasync_show_formasync_show_form(
142  step_id="local",
143  description_placeholders={
144  "url": "https://support.getawair.com/hc/en-us/articles/360049221014-Awair-Element-Local-API-Feature#h_01F40FBBW5323GBPV7D6XMG4J8"
145  },
146  )
147 
149  self, user_input: Mapping[str, Any] | None = None
150  ) -> ConfigFlowResult:
151  """Handle collecting and verifying Awair Local API hosts."""
152 
153  errors = {}
154 
155  # User input is either:
156  # 1. None if first time on this step
157  # 2. {device: manual} if picked manual entry option
158  # 3. {device: <host>} if picked a device
159  # 4. {host: <host>} if manually entered a host
160  #
161  # Option 1 and 2 will show the form again.
162  if user_input and user_input.get(CONF_DEVICE) != "manual":
163  if CONF_DEVICE in user_input:
164  user_input = {CONF_HOST: user_input[CONF_DEVICE]}
165 
166  self._device, error = await self._check_local_connection_check_local_connection(
167  user_input.get(CONF_DEVICE) or user_input[CONF_HOST]
168  )
169 
170  if self._device is not None:
171  await self.async_set_unique_idasync_set_unique_id(
172  self._device.mac_address, raise_on_progress=False
173  )
174  title = f"{self._device.model} ({self._device.device_id})"
175  return self.async_create_entryasync_create_entryasync_create_entry(title=title, data=user_input)
176 
177  if error is not None:
178  errors = {"base": error}
179 
180  discovered = self._get_discovered_entries_get_discovered_entries()
181 
182  if not discovered or (user_input and user_input.get(CONF_DEVICE) == "manual"):
183  data_schema = vol.Schema({vol.Required(CONF_HOST): str})
184 
185  elif discovered:
186  discovered["manual"] = "Manual"
187  data_schema = vol.Schema({vol.Required(CONF_DEVICE): vol.In(discovered)})
188 
189  return self.async_show_formasync_show_formasync_show_form(
190  step_id="local_pick",
191  data_schema=data_schema,
192  errors=errors,
193  )
194 
195  async def async_step_reauth(
196  self, entry_data: Mapping[str, Any]
197  ) -> ConfigFlowResult:
198  """Handle re-auth if token invalid."""
199  return await self.async_step_reauth_confirmasync_step_reauth_confirm()
200 
202  self, user_input: dict[str, Any] | None = None
203  ) -> ConfigFlowResult:
204  """Confirm reauth dialog."""
205  errors = {}
206 
207  if user_input is not None:
208  access_token = user_input[CONF_ACCESS_TOKEN]
209  _, error = await self._check_cloud_connection_check_cloud_connection(access_token)
210 
211  if error is None:
212  return self.async_update_reload_and_abortasync_update_reload_and_abort(
213  self._get_reauth_entry_get_reauth_entry(), data_updates=user_input
214  )
215 
216  if error != "invalid_access_token":
217  return self.async_abortasync_abortasync_abort(reason=error)
218 
219  errors = {CONF_ACCESS_TOKEN: error}
220 
221  return self.async_show_formasync_show_formasync_show_form(
222  step_id="reauth_confirm",
223  data_schema=vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}),
224  errors=errors,
225  )
226 
228  self, device_address: str
229  ) -> tuple[AwairLocalDevice | None, str | None]:
230  """Check the access token is valid."""
231  session = async_get_clientsession(self.hass)
232  awair = AwairLocal(session=session, device_addrs=[device_address])
233 
234  try:
235  devices = await awair.devices()
236  return (devices[0], None)
237 
238  except ClientError as err:
239  LOGGER.error("Unable to connect error: %s", err)
240  return (None, "unreachable")
241 
242  except AwairError as err:
243  LOGGER.error("Unexpected API error: %s", err)
244  return (None, "unknown")
245 
247  self, access_token: str
248  ) -> tuple[AwairUser | None, str | None]:
249  """Check the access token is valid."""
250  session = async_get_clientsession(self.hass)
251  awair = Awair(access_token=access_token, session=session)
252 
253  try:
254  user = await awair.user()
255  devices = await user.devices()
256  except AuthError:
257  return (None, "invalid_access_token")
258  except AwairError as err:
259  LOGGER.error("Unexpected API error: %s", err)
260  return (None, "unknown")
261 
262  if not devices:
263  return (None, "no_devices_found")
264  return (user, None)
ConfigFlowResult async_step_user(self, dict[str, str]|None user_input=None)
Definition: config_flow.py:82
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:33
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:203
ConfigFlowResult async_step_discovery_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:61
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:197
ConfigFlowResult async_step_local(self, Mapping[str, Any]|None user_input=None)
Definition: config_flow.py:136
tuple[AwairLocalDevice|None, str|None] _check_local_connection(self, str device_address)
Definition: config_flow.py:229
tuple[AwairUser|None, str|None] _check_cloud_connection(self, str access_token)
Definition: config_flow.py:248
ConfigFlowResult async_step_cloud(self, Mapping[str, Any] user_input)
Definition: config_flow.py:87
ConfigFlowResult async_step_local_pick(self, Mapping[str, Any]|None user_input=None)
Definition: config_flow.py:150
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_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_abort(self, *str reason, Mapping[str, str]|None description_placeholders=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)
_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_show_menu(self, *str|None step_id=None, Container[str] menu_options, Mapping[str, str]|None description_placeholders=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)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
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)