Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Ring integration."""
2 
3 from collections.abc import Mapping
4 import logging
5 from typing import Any
6 import uuid
7 
8 from ring_doorbell import Auth, AuthenticationError, Requires2FAError
9 import voluptuous as vol
10 
11 from homeassistant.components import dhcp
12 from homeassistant.config_entries import (
13  SOURCE_REAUTH,
14  SOURCE_RECONFIGURE,
15  ConfigFlow,
16  ConfigFlowResult,
17 )
18 from homeassistant.const import (
19  CONF_DEVICE_ID,
20  CONF_NAME,
21  CONF_PASSWORD,
22  CONF_TOKEN,
23  CONF_USERNAME,
24 )
25 from homeassistant.core import HomeAssistant
26 from homeassistant.exceptions import HomeAssistantError
27 from homeassistant.helpers.aiohttp_client import async_get_clientsession
29 
30 from . import get_auth_user_agent
31 from .const import CONF_2FA, CONF_CONFIG_ENTRY_MINOR_VERSION, DOMAIN
32 
33 _LOGGER = logging.getLogger(__name__)
34 
35 STEP_USER_DATA_SCHEMA = vol.Schema(
36  {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
37 )
38 STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
39 
40 STEP_RECONFIGURE_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
41 
42 UNKNOWN_RING_ACCOUNT = "unknown_ring_account"
43 
44 
45 async def validate_input(
46  hass: HomeAssistant, hardware_id: str, data: dict[str, str]
47 ) -> dict[str, Any]:
48  """Validate the user input allows us to connect."""
49 
50  user_agent = get_auth_user_agent()
51  auth = Auth(
52  user_agent,
53  http_client_session=async_get_clientsession(hass),
54  hardware_id=hardware_id,
55  )
56 
57  try:
58  token = await auth.async_fetch_token(
59  data[CONF_USERNAME],
60  data[CONF_PASSWORD],
61  data.get(CONF_2FA),
62  )
63  except Requires2FAError as err:
64  raise Require2FA from err
65  except AuthenticationError as err:
66  raise InvalidAuth from err
67 
68  return token
69 
70 
71 class RingConfigFlow(ConfigFlow, domain=DOMAIN):
72  """Handle a config flow for Ring."""
73 
74  VERSION = 1
75  MINOR_VERSION = CONF_CONFIG_ENTRY_MINOR_VERSION
76 
77  user_pass: dict[str, Any] = {}
78  hardware_id: str | None = None
79 
80  async def async_step_dhcp(
81  self, discovery_info: dhcp.DhcpServiceInfo
82  ) -> ConfigFlowResult:
83  """Handle discovery via dhcp."""
84  # Ring has a single config entry per cloud username rather than per device
85  # so we check whether that device is already configured.
86  # If the device is not configured there's either no ring config entry
87  # yet or the device is registered to a different account
88  await self.async_set_unique_idasync_set_unique_id(UNKNOWN_RING_ACCOUNT)
89  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
90  if self.hass.config_entries.async_has_entries(DOMAIN):
91  device_registry = dr.async_get(self.hass)
92  if device_registry.async_get_device(
93  identifiers={(DOMAIN, discovery_info.macaddress)}
94  ):
95  return self.async_abortasync_abortasync_abort(reason="already_configured")
96 
97  return await self.async_step_userasync_step_user()
98 
99  async def async_step_user(
100  self, user_input: dict[str, Any] | None = None
101  ) -> ConfigFlowResult:
102  """Handle the initial step."""
103  errors: dict[str, str] = {}
104  if user_input is not None:
105  await self.async_set_unique_idasync_set_unique_id(user_input[CONF_USERNAME])
106  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
107  if not self.hardware_idhardware_id:
108  self.hardware_idhardware_id = str(uuid.uuid4())
109  try:
110  token = await validate_input(self.hass, self.hardware_idhardware_id, user_input)
111  except Require2FA:
112  self.user_passuser_pass = user_input
113 
114  return await self.async_step_2fa()
115  except InvalidAuth:
116  errors["base"] = "invalid_auth"
117  except Exception:
118  _LOGGER.exception("Unexpected exception")
119  errors["base"] = "unknown"
120  else:
121  return self.async_create_entryasync_create_entryasync_create_entry(
122  title=user_input[CONF_USERNAME],
123  data={
124  CONF_DEVICE_ID: self.hardware_idhardware_id,
125  CONF_USERNAME: user_input[CONF_USERNAME],
126  CONF_TOKEN: token,
127  },
128  )
129 
130  return self.async_show_formasync_show_formasync_show_form(
131  step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
132  )
133 
134  async def async_step_2fa(
135  self, user_input: dict[str, Any] | None = None
136  ) -> ConfigFlowResult:
137  """Handle 2fa step."""
138  if user_input:
139  if self.sourcesourcesourcesource == SOURCE_REAUTH:
140  return await self.async_step_reauth_confirm(
141  {**self.user_passuser_pass, **user_input}
142  )
143 
144  if self.sourcesourcesourcesource == SOURCE_RECONFIGURE:
145  return await self.async_step_reconfigure(
146  {**self.user_passuser_pass, **user_input}
147  )
148 
149  return await self.async_step_userasync_step_user({**self.user_passuser_pass, **user_input})
150 
151  return self.async_show_formasync_show_formasync_show_form(
152  step_id="2fa",
153  data_schema=vol.Schema({vol.Required(CONF_2FA): str}),
154  )
155 
156  async def async_step_reauth(
157  self, entry_data: Mapping[str, Any]
158  ) -> ConfigFlowResult:
159  """Handle reauth upon an API authentication error."""
160  return await self.async_step_reauth_confirm()
161 
162  async def async_step_reauth_confirm(
163  self, user_input: dict[str, Any] | None = None
164  ) -> ConfigFlowResult:
165  """Dialog that informs the user that reauth is required."""
166  errors: dict[str, str] = {}
167 
168  reauth_entry = self._get_reauth_entry_get_reauth_entry()
169  if user_input:
170  user_input[CONF_USERNAME] = reauth_entry.data[CONF_USERNAME]
171  # Reauth will use the same hardware id and re-authorise an existing
172  # authorised device.
173  if not self.hardware_idhardware_id:
174  self.hardware_idhardware_id = reauth_entry.data[CONF_DEVICE_ID]
175  assert self.hardware_idhardware_id
176  try:
177  token = await validate_input(self.hass, self.hardware_idhardware_id, user_input)
178  except Require2FA:
179  self.user_passuser_pass = user_input
180  return await self.async_step_2fa()
181  except InvalidAuth:
182  errors["base"] = "invalid_auth"
183  except Exception:
184  _LOGGER.exception("Unexpected exception")
185  errors["base"] = "unknown"
186  else:
187  data = {
188  CONF_USERNAME: user_input[CONF_USERNAME],
189  CONF_TOKEN: token,
190  CONF_DEVICE_ID: self.hardware_idhardware_id,
191  }
192  return self.async_update_reload_and_abortasync_update_reload_and_abort(reauth_entry, data=data)
193 
194  return self.async_show_formasync_show_formasync_show_form(
195  step_id="reauth_confirm",
196  data_schema=STEP_REAUTH_DATA_SCHEMA,
197  errors=errors,
198  description_placeholders={
199  CONF_USERNAME: reauth_entry.data[CONF_USERNAME],
200  CONF_NAME: reauth_entry.data[CONF_USERNAME],
201  },
202  )
203 
204  async def async_step_reconfigure(
205  self, user_input: dict[str, Any] | None = None
206  ) -> ConfigFlowResult:
207  """Trigger a reconfiguration flow."""
208  errors: dict[str, str] = {}
209  reconfigure_entry = self._get_reconfigure_entry_get_reconfigure_entry()
210  username = reconfigure_entry.data[CONF_USERNAME]
211  await self.async_set_unique_idasync_set_unique_id(username)
212  if user_input:
213  user_input[CONF_USERNAME] = username
214  # Reconfigure will generate a new hardware id and create a new
215  # authorised device at ring.com.
216  if not self.hardware_idhardware_id:
217  self.hardware_idhardware_id = str(uuid.uuid4())
218  try:
219  assert self.hardware_idhardware_id
220  token = await validate_input(self.hass, self.hardware_idhardware_id, user_input)
221  except Require2FA:
222  self.user_passuser_pass = user_input
223  return await self.async_step_2fa()
224  except InvalidAuth:
225  errors["base"] = "invalid_auth"
226  except Exception:
227  _LOGGER.exception("Unexpected exception")
228  errors["base"] = "unknown"
229  else:
230  data = {
231  CONF_USERNAME: username,
232  CONF_TOKEN: token,
233  CONF_DEVICE_ID: self.hardware_idhardware_id,
234  }
235  return self.async_update_reload_and_abortasync_update_reload_and_abort(reconfigure_entry, data=data)
236 
237  return self.async_show_formasync_show_formasync_show_form(
238  step_id="reconfigure",
239  data_schema=STEP_RECONFIGURE_DATA_SCHEMA,
240  errors=errors,
241  description_placeholders={
242  CONF_USERNAME: username,
243  },
244  )
245 
246 
248  """Error to indicate we require 2FA."""
249 
250 
252  """Error to indicate there is invalid auth."""
ConfigFlowResult async_step_dhcp(self, dhcp.DhcpServiceInfo discovery_info)
Definition: config_flow.py:82
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_step_user(self, dict[str, Any]|None user_input=None)
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)
str
_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)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
dict[str, Any] validate_input(HomeAssistant hass, str hardware_id, dict[str, str] data)
Definition: config_flow.py:47
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)