Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for One-Time Password (OTP) integration."""
2 
3 from __future__ import annotations
4 
5 import binascii
6 import logging
7 from re import sub
8 from typing import Any
9 
10 import pyotp
11 import voluptuous as vol
12 
13 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
14 from homeassistant.const import CONF_CODE, CONF_NAME, CONF_TOKEN
16  BooleanSelector,
17  BooleanSelectorConfig,
18  QrCodeSelector,
19  QrCodeSelectorConfig,
20  QrErrorCorrectionLevel,
21 )
22 
23 from .const import CONF_NEW_TOKEN, DEFAULT_NAME, DOMAIN
24 
25 _LOGGER = logging.getLogger(__name__)
26 
27 STEP_USER_DATA_SCHEMA = vol.Schema(
28  {
29  vol.Optional(CONF_TOKEN): str,
30  vol.Optional(CONF_NEW_TOKEN): BooleanSelector(BooleanSelectorConfig()),
31  vol.Required(CONF_NAME, default=DEFAULT_NAME): str,
32  }
33 )
34 
35 STEP_CONFIRM_DATA_SCHEMA = vol.Schema({vol.Required(CONF_CODE): str})
36 
37 
38 class TOTPConfigFlow(ConfigFlow, domain=DOMAIN):
39  """Handle a config flow for One-Time Password (OTP)."""
40 
41  VERSION = 1
42  user_input: dict[str, Any]
43 
44  async def async_step_user(
45  self, user_input: dict[str, Any] | None = None
46  ) -> ConfigFlowResult:
47  """Handle the initial step."""
48  errors: dict[str, str] = {}
49  if user_input is not None:
50  if user_input.get(CONF_TOKEN) and not user_input.get(CONF_NEW_TOKEN):
51  user_input[CONF_TOKEN] = sub(r"\s+", "", user_input[CONF_TOKEN])
52  try:
53  await self.hass.async_add_executor_job(
54  pyotp.TOTP(user_input[CONF_TOKEN]).now
55  )
56  except binascii.Error:
57  errors["base"] = "invalid_token"
58  except Exception:
59  _LOGGER.exception("Unexpected exception")
60  errors["base"] = "unknown"
61  else:
62  await self.async_set_unique_idasync_set_unique_id(user_input[CONF_TOKEN])
63  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
64  return self.async_create_entryasync_create_entryasync_create_entry(
65  title=user_input[CONF_NAME],
66  data=user_input,
67  )
68  elif user_input.get(CONF_NEW_TOKEN):
69  user_input[CONF_TOKEN] = await self.hass.async_add_executor_job(
70  pyotp.random_base32
71  )
72  self.user_inputuser_input = user_input
73  return await self.async_step_confirmasync_step_confirm()
74  else:
75  errors["base"] = "invalid_token"
76 
77  return self.async_show_formasync_show_formasync_show_form(
78  step_id="user",
79  data_schema=self.add_suggested_values_to_schemaadd_suggested_values_to_schema(
80  data_schema=STEP_USER_DATA_SCHEMA, suggested_values=user_input
81  ),
82  errors=errors,
83  )
84 
85  async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
86  """Import config from yaml."""
87 
88  await self.async_set_unique_idasync_set_unique_id(import_data[CONF_TOKEN])
89  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
90 
91  return self.async_create_entryasync_create_entryasync_create_entry(
92  title=import_data.get(CONF_NAME, DEFAULT_NAME),
93  data=import_data,
94  )
95 
96  async def async_step_confirm(
97  self, user_input: dict[str, Any] | None = None
98  ) -> ConfigFlowResult:
99  """Handle the confirmation step."""
100 
101  errors: dict[str, str] = {}
102 
103  if user_input is not None:
104  if await self.hass.async_add_executor_job(
105  pyotp.TOTP(self.user_inputuser_input[CONF_TOKEN]).verify, user_input["code"]
106  ):
107  return self.async_create_entryasync_create_entryasync_create_entry(
108  title=self.user_inputuser_input[CONF_NAME],
109  data={
110  CONF_NAME: self.user_inputuser_input[CONF_NAME],
111  CONF_TOKEN: self.user_inputuser_input[CONF_TOKEN],
112  },
113  )
114 
115  errors["base"] = "invalid_code"
116 
117  provisioning_uri = await self.hass.async_add_executor_job(
118  pyotp.TOTP(self.user_inputuser_input[CONF_TOKEN]).provisioning_uri,
119  self.user_inputuser_input[CONF_NAME],
120  "Home Assistant",
121  )
122  data_schema = STEP_CONFIRM_DATA_SCHEMA.extend(
123  {
124  vol.Optional("qr_code"): QrCodeSelector(
125  config=QrCodeSelectorConfig(
126  data=provisioning_uri,
127  scale=6,
128  error_correction_level=QrErrorCorrectionLevel.QUARTILE,
129  )
130  )
131  }
132  )
133  return self.async_show_formasync_show_formasync_show_form(
134  step_id="confirm",
135  data_schema=data_schema,
136  description_placeholders={
137  "auth_app1": "[Google Authenticator](https://support.google.com/accounts/answer/1066447)",
138  "auth_app2": "[Authy](https://authy.com/)",
139  "code": self.user_inputuser_input[CONF_TOKEN],
140  },
141  errors=errors,
142  )
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:46
ConfigFlowResult async_step_import(self, dict[str, Any] import_data)
Definition: config_flow.py:85
ConfigFlowResult async_step_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:98
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)
vol.Schema add_suggested_values_to_schema(self, vol.Schema data_schema, Mapping[str, Any]|None suggested_values)
_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)