Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow to configure Blink."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 import logging
7 from typing import Any
8 
9 from blinkpy.auth import Auth, LoginError, TokenRefreshFailed
10 from blinkpy.blinkpy import Blink, BlinkSetupError
11 import voluptuous as vol
12 
13 from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
14 from homeassistant.const import CONF_PASSWORD, CONF_PIN, CONF_USERNAME
15 from homeassistant.core import HomeAssistant, callback
16 from homeassistant.exceptions import HomeAssistantError
17 from homeassistant.helpers.aiohttp_client import async_get_clientsession
18 
19 from .const import DEVICE_ID, DOMAIN
20 
21 _LOGGER = logging.getLogger(__name__)
22 
23 
24 async def validate_input(auth: Auth) -> None:
25  """Validate the user input allows us to connect."""
26  try:
27  await auth.startup()
28  except (LoginError, TokenRefreshFailed) as err:
29  raise InvalidAuth from err
30  if auth.check_key_required():
31  raise Require2FA
32 
33 
34 async def _send_blink_2fa_pin(hass: HomeAssistant, auth: Auth, pin: str | None) -> bool:
35  """Send 2FA pin to blink servers."""
36  blink = Blink(session=async_get_clientsession(hass))
37  blink.auth = auth
38  blink.setup_login_ids()
39  blink.setup_urls()
40  return await auth.send_auth_key(blink, pin)
41 
42 
43 class BlinkConfigFlow(ConfigFlow, domain=DOMAIN):
44  """Handle a Blink config flow."""
45 
46  VERSION = 3
47 
48  def __init__(self) -> None:
49  """Initialize the blink flow."""
50  self.authauth: Auth | None = None
51 
52  async def async_step_user(
53  self, user_input: dict[str, Any] | None = None
54  ) -> ConfigFlowResult:
55  """Handle a flow initiated by the user."""
56  errors = {}
57  if user_input is not None:
58  self.authauth = Auth(
59  {**user_input, "device_id": DEVICE_ID},
60  no_prompt=True,
61  session=async_get_clientsession(self.hass),
62  )
63  await self.async_set_unique_idasync_set_unique_id(user_input[CONF_USERNAME])
64  if self.sourcesourcesource != SOURCE_REAUTH:
65  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
66 
67  try:
68  await validate_input(self.authauth)
69  return self._async_finish_flow_async_finish_flow()
70  except Require2FA:
71  return await self.async_step_2faasync_step_2fa()
72  except InvalidAuth:
73  errors["base"] = "invalid_auth"
74  except Exception:
75  _LOGGER.exception("Unexpected exception")
76  errors["base"] = "unknown"
77 
78  return self.async_show_formasync_show_formasync_show_form(
79  step_id="user",
80  data_schema=vol.Schema(
81  {
82  vol.Required(CONF_USERNAME): str,
83  vol.Required(CONF_PASSWORD): str,
84  }
85  ),
86  errors=errors,
87  )
88 
89  async def async_step_2fa(
90  self, user_input: dict[str, Any] | None = None
91  ) -> ConfigFlowResult:
92  """Handle 2FA step."""
93  errors = {}
94  if user_input is not None:
95  try:
96  valid_token = await _send_blink_2fa_pin(
97  self.hass, self.authauth, user_input.get(CONF_PIN)
98  )
99  except BlinkSetupError:
100  errors["base"] = "cannot_connect"
101  except Exception:
102  _LOGGER.exception("Unexpected exception")
103  errors["base"] = "unknown"
104 
105  else:
106  if valid_token:
107  return self._async_finish_flow_async_finish_flow()
108  errors["base"] = "invalid_access_token"
109 
110  return self.async_show_formasync_show_formasync_show_form(
111  step_id="2fa",
112  data_schema=vol.Schema(
113  {vol.Optional(CONF_PIN): vol.All(str, vol.Length(min=1))}
114  ),
115  errors=errors,
116  )
117 
118  async def async_step_reauth(
119  self, entry_data: Mapping[str, Any]
120  ) -> ConfigFlowResult:
121  """Perform reauth upon migration of old entries."""
122  return await self.async_step_userasync_step_userasync_step_user(dict(entry_data))
123 
124  @callback
125  def _async_finish_flow(self) -> ConfigFlowResult:
126  """Finish with setup."""
127  assert self.authauth
128  return self.async_create_entryasync_create_entryasync_create_entry(title=DOMAIN, data=self.authauth.login_attributes)
129 
130 
132  """Error to indicate we require 2FA."""
133 
134 
135 class InvalidAuth(HomeAssistantError):
136  """Error to indicate there is invalid auth."""
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_step_user(self, dict[str, Any]|None user_input=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_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
str|None source(self)
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)