Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Coinbase integration."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 
8 from coinbase.rest import RESTClient
9 from coinbase.rest.rest_base import HTTPError
10 from coinbase.wallet.client import Client as LegacyClient
11 from coinbase.wallet.error import AuthenticationError
12 import voluptuous as vol
13 
14 from homeassistant.config_entries import (
15  ConfigEntry,
16  ConfigFlow,
17  ConfigFlowResult,
18  OptionsFlow,
19 )
20 from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, CONF_API_VERSION
21 from homeassistant.core import HomeAssistant, callback
22 from homeassistant.exceptions import HomeAssistantError
24 
25 from . import get_accounts
26 from .const import (
27  ACCOUNT_IS_VAULT,
28  API_ACCOUNT_CURRENCY,
29  API_DATA,
30  API_RATES,
31  CONF_CURRENCIES,
32  CONF_EXCHANGE_BASE,
33  CONF_EXCHANGE_PRECISION,
34  CONF_EXCHANGE_PRECISION_DEFAULT,
35  CONF_EXCHANGE_RATES,
36  DOMAIN,
37  RATES,
38  WALLETS,
39 )
40 
41 _LOGGER = logging.getLogger(__name__)
42 
43 STEP_USER_DATA_SCHEMA = vol.Schema(
44  {
45  vol.Required(CONF_API_KEY): str,
46  vol.Required(CONF_API_TOKEN): str,
47  }
48 )
49 
50 
51 def get_user_from_client(api_key, api_token):
52  """Get the user name from Coinbase API credentials."""
53  if "organizations" not in api_key:
54  client = LegacyClient(api_key, api_token)
55  return client.get_current_user()["name"]
56  client = RESTClient(api_key=api_key, api_secret=api_token)
57  return client.get_portfolios()["portfolios"][0]["name"]
58 
59 
60 async def validate_api(hass: HomeAssistant, data):
61  """Validate the credentials."""
62 
63  try:
64  user = await hass.async_add_executor_job(
65  get_user_from_client, data[CONF_API_KEY], data[CONF_API_TOKEN]
66  )
67  except (AuthenticationError, HTTPError) as error:
68  if "api key" in str(error) or " 401 Client Error" in str(error):
69  _LOGGER.debug("Coinbase rejected API credentials due to an invalid API key")
70  raise InvalidKey from error
71  if "invalid signature" in str(
72  error
73  ) or "'Could not deserialize key data" in str(error):
74  _LOGGER.debug(
75  "Coinbase rejected API credentials due to an invalid API secret"
76  )
77  raise InvalidSecret from error
78  _LOGGER.debug("Coinbase rejected API credentials due to an unknown error")
79  raise InvalidAuth from error
80  except ConnectionError as error:
81  raise CannotConnect from error
82  api_version = "v3" if "organizations" in data[CONF_API_KEY] else "v2"
83  return {"title": user, "api_version": api_version}
84 
85 
86 async def validate_options(hass: HomeAssistant, config_entry: ConfigEntry, options):
87  """Validate the requested resources are provided by API."""
88 
89  client = hass.data[DOMAIN][config_entry.entry_id].client
90 
91  accounts = await hass.async_add_executor_job(
92  get_accounts, client, config_entry.data.get("api_version", "v2")
93  )
94 
95  accounts_currencies = [
96  account[API_ACCOUNT_CURRENCY]
97  for account in accounts
98  if not account[ACCOUNT_IS_VAULT]
99  ]
100  if config_entry.data.get("api_version", "v2") == "v2":
101  available_rates = await hass.async_add_executor_job(client.get_exchange_rates)
102  else:
103  resp = await hass.async_add_executor_job(client.get, "/v2/exchange-rates")
104  available_rates = resp[API_DATA]
105  if CONF_CURRENCIES in options:
106  for currency in options[CONF_CURRENCIES]:
107  if currency not in accounts_currencies:
108  raise CurrencyUnavailable
109 
110  if CONF_EXCHANGE_RATES in options:
111  for rate in options[CONF_EXCHANGE_RATES]:
112  if rate not in available_rates[API_RATES]:
113  raise ExchangeRateUnavailable
114 
115  return True
116 
117 
118 class CoinbaseConfigFlow(ConfigFlow, domain=DOMAIN):
119  """Handle a config flow for Coinbase."""
120 
121  VERSION = 1
122 
123  async def async_step_user(
124  self, user_input: dict[str, str] | None = None
125  ) -> ConfigFlowResult:
126  """Handle the initial step."""
127  errors: dict[str, str] = {}
128  if user_input is None:
129  return self.async_show_formasync_show_formasync_show_form(
130  step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
131  )
132 
133  self._async_abort_entries_match_async_abort_entries_match({CONF_API_KEY: user_input[CONF_API_KEY]})
134 
135  try:
136  info = await validate_api(self.hass, user_input)
137  except CannotConnect:
138  errors["base"] = "cannot_connect"
139  except InvalidKey:
140  errors["base"] = "invalid_auth_key"
141  except InvalidSecret:
142  errors["base"] = "invalid_auth_secret"
143  except InvalidAuth:
144  errors["base"] = "invalid_auth"
145  except Exception:
146  _LOGGER.exception("Unexpected exception")
147  errors["base"] = "unknown"
148  else:
149  user_input[CONF_API_VERSION] = info["api_version"]
150  return self.async_create_entryasync_create_entryasync_create_entry(title=info["title"], data=user_input)
151  return self.async_show_formasync_show_formasync_show_form(
152  step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
153  )
154 
155  @staticmethod
156  @callback
158  config_entry: ConfigEntry,
159  ) -> OptionsFlowHandler:
160  """Get the options flow for this handler."""
161  return OptionsFlowHandler()
162 
163 
165  """Handle a option flow for Coinbase."""
166 
167  async def async_step_init(
168  self, user_input: dict[str, Any] | None = None
169  ) -> ConfigFlowResult:
170  """Manage the options."""
171 
172  errors = {}
173  default_currencies = self.config_entryconfig_entryconfig_entry.options.get(CONF_CURRENCIES, [])
174  default_exchange_rates = self.config_entryconfig_entryconfig_entry.options.get(CONF_EXCHANGE_RATES, [])
175  default_exchange_base = self.config_entryconfig_entryconfig_entry.options.get(CONF_EXCHANGE_BASE, "USD")
176  default_exchange_precision = self.config_entryconfig_entryconfig_entry.options.get(
177  CONF_EXCHANGE_PRECISION, CONF_EXCHANGE_PRECISION_DEFAULT
178  )
179 
180  if user_input is not None:
181  # Pass back user selected options, even if bad
182  if CONF_CURRENCIES in user_input:
183  default_currencies = user_input[CONF_CURRENCIES]
184 
185  if CONF_EXCHANGE_RATES in user_input:
186  default_exchange_rates = user_input[CONF_EXCHANGE_RATES]
187 
188  if CONF_EXCHANGE_RATES in user_input:
189  default_exchange_base = user_input[CONF_EXCHANGE_BASE]
190 
191  if CONF_EXCHANGE_PRECISION in user_input:
192  default_exchange_precision = user_input[CONF_EXCHANGE_PRECISION]
193 
194  try:
195  await validate_options(self.hass, self.config_entryconfig_entryconfig_entry, user_input)
196  except CurrencyUnavailable:
197  errors["base"] = "currency_unavailable"
198  except ExchangeRateUnavailable:
199  errors["base"] = "exchange_rate_unavailable"
200  except Exception:
201  _LOGGER.exception("Unexpected exception")
202  errors["base"] = "unknown"
203  else:
204  return self.async_create_entryasync_create_entry(title="", data=user_input)
205 
206  return self.async_show_formasync_show_form(
207  step_id="init",
208  data_schema=vol.Schema(
209  {
210  vol.Optional(
211  CONF_CURRENCIES,
212  default=default_currencies,
213  ): cv.multi_select(WALLETS),
214  vol.Optional(
215  CONF_EXCHANGE_RATES,
216  default=default_exchange_rates,
217  ): cv.multi_select(RATES),
218  vol.Optional(
219  CONF_EXCHANGE_BASE,
220  default=default_exchange_base,
221  ): vol.In(WALLETS),
222  vol.Optional(
223  CONF_EXCHANGE_PRECISION, default=default_exchange_precision
224  ): int,
225  }
226  ),
227  errors=errors,
228  )
229 
230 
232  """Error to indicate we cannot connect."""
233 
234 
235 class InvalidAuth(HomeAssistantError):
236  """Error to indicate there is invalid auth."""
237 
238 
240  """Error to indicate auth failed due to invalid secret."""
241 
242 
244  """Error to indicate auth failed due to invalid key."""
245 
246 
248  """Error to indicate Coinbase API Key is already configured."""
249 
250 
252  """Error to indicate the requested currency resource is not provided by the API."""
253 
254 
256  """Error to indicate the requested exchange rate resource is not provided by the API."""
OptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:159
ConfigFlowResult async_step_user(self, dict[str, str]|None user_input=None)
Definition: config_flow.py:125
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:169
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)
None _async_abort_entries_match(self, dict[str, Any]|None match_dict=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)
None config_entry(self, ConfigEntry value)
_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)
def get_user_from_client(api_key, api_token)
Definition: config_flow.py:51
def validate_api(HomeAssistant hass, data)
Definition: config_flow.py:60
def validate_options(HomeAssistant hass, ConfigEntry config_entry, options)
Definition: config_flow.py:86