Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Monarch Money integration."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 
8 from monarchmoney import LoginFailedException, RequireMFAException
9 from monarchmoney.monarchmoney import SESSION_FILE
10 from typedmonarchmoney import TypedMonarchMoney
11 from typedmonarchmoney.models import MonarchSubscription
12 import voluptuous as vol
13 
14 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
15 from homeassistant.const import CONF_EMAIL, CONF_ID, CONF_PASSWORD, CONF_TOKEN
16 from homeassistant.core import HomeAssistant
17 from homeassistant.exceptions import HomeAssistantError
19  TextSelector,
20  TextSelectorConfig,
21  TextSelectorType,
22 )
23 
24 from .const import CONF_MFA_CODE, DOMAIN, LOGGER
25 
26 _LOGGER = logging.getLogger(__name__)
27 
28 
29 STEP_USER_DATA_SCHEMA = vol.Schema(
30  {
31  vol.Required(CONF_EMAIL): TextSelector(
33  type=TextSelectorType.EMAIL,
34  ),
35  ),
36  vol.Required(CONF_PASSWORD): TextSelector(
38  type=TextSelectorType.PASSWORD,
39  ),
40  ),
41  }
42 )
43 
44 STEP_MFA_DATA_SCHEMA = vol.Schema(
45  {
46  vol.Required(CONF_MFA_CODE): str,
47  }
48 )
49 
50 
51 async def validate_login(
52  hass: HomeAssistant,
53  data: dict[str, Any],
54  email: str | None = None,
55  password: str | None = None,
56 ) -> dict[str, Any]:
57  """Validate the user input allows us to connect.
58 
59  Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. Upon success a session will be saved
60  """
61 
62  if not email:
63  email = data[CONF_EMAIL]
64  if not password:
65  password = data[CONF_PASSWORD]
66  monarch_client = TypedMonarchMoney()
67  if CONF_MFA_CODE in data:
68  mfa_code = data[CONF_MFA_CODE]
69  LOGGER.debug("Attempting to authenticate with MFA code")
70  try:
71  await monarch_client.multi_factor_authenticate(email, password, mfa_code)
72  except KeyError as err:
73  # A bug in the backing lib that I don't control throws a KeyError if the MFA code is wrong
74  LOGGER.debug("Bad MFA Code")
75  raise BadMFA from err
76  else:
77  LOGGER.debug("Attempting to authenticate")
78  try:
79  await monarch_client.login(
80  email=email,
81  password=password,
82  save_session=False,
83  use_saved_session=False,
84  )
85  except RequireMFAException:
86  raise
87  except LoginFailedException as err:
88  raise InvalidAuth from err
89 
90  LOGGER.debug(f"Connection successful - saving session to file {SESSION_FILE}")
91  LOGGER.debug("Obtaining subscription id")
92  subs: MonarchSubscription = await monarch_client.get_subscription_details()
93  assert subs is not None
94  subscription_id = subs.id
95  return {
96  CONF_TOKEN: monarch_client.token,
97  CONF_ID: subscription_id,
98  }
99 
100 
101 class MonarchMoneyConfigFlow(ConfigFlow, domain=DOMAIN):
102  """Handle a config flow for Monarch Money."""
103 
104  VERSION = 1
105 
106  def __init__(self) -> None:
107  """Initialize config flow."""
108  self.emailemail: str | None = None
109  self.passwordpassword: str | None = None
110 
111  async def async_step_user(
112  self, user_input: dict[str, Any] | None = None
113  ) -> ConfigFlowResult:
114  """Handle the initial step."""
115  errors: dict[str, str] = {}
116 
117  if user_input is not None:
118  try:
119  info = await validate_login(
120  self.hass, user_input, email=self.emailemail, password=self.passwordpassword
121  )
122  except RequireMFAException:
123  self.emailemail = user_input[CONF_EMAIL]
124  self.passwordpassword = user_input[CONF_PASSWORD]
125 
126  return self.async_show_formasync_show_formasync_show_form(
127  step_id="user",
128  data_schema=STEP_MFA_DATA_SCHEMA,
129  errors={"base": "mfa_required"},
130  )
131  except BadMFA:
132  return self.async_show_formasync_show_formasync_show_form(
133  step_id="user",
134  data_schema=STEP_MFA_DATA_SCHEMA,
135  errors={"base": "bad_mfa"},
136  )
137  except InvalidAuth:
138  errors["base"] = "invalid_auth"
139  else:
140  await self.async_set_unique_idasync_set_unique_id(info[CONF_ID])
141  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
142 
143  return self.async_create_entryasync_create_entryasync_create_entry(
144  title="Monarch Money",
145  data={CONF_TOKEN: info[CONF_TOKEN]},
146  )
147  return self.async_show_formasync_show_formasync_show_form(
148  step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
149  )
150 
151 
153  """Error to indicate there is invalid auth."""
154 
155 
156 class BadMFA(HomeAssistantError):
157  """Error to indicate the MFA code was bad."""
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:113
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)
_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)
dict[str, Any] validate_login(HomeAssistant hass, dict[str, Any] data, str|None email=None, str|None password=None)
Definition: config_flow.py:56