Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow to configure SmartThings."""
2 
3 from http import HTTPStatus
4 import logging
5 from typing import Any
6 
7 from aiohttp import ClientResponseError
8 from pysmartthings import APIResponseError, AppOAuth, SmartThings
9 from pysmartthings.installedapp import format_install_url
10 import voluptuous as vol
11 
12 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
13 from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET
14 from homeassistant.helpers.aiohttp_client import async_get_clientsession
15 
16 from .const import (
17  APP_OAUTH_CLIENT_NAME,
18  APP_OAUTH_SCOPES,
19  CONF_APP_ID,
20  CONF_INSTALLED_APP_ID,
21  CONF_LOCATION_ID,
22  CONF_REFRESH_TOKEN,
23  DOMAIN,
24  VAL_UID_MATCHER,
25 )
26 from .smartapp import (
27  create_app,
28  find_app,
29  format_unique_id,
30  get_webhook_url,
31  setup_smartapp,
32  setup_smartapp_endpoint,
33  update_app,
34  validate_webhook_requirements,
35 )
36 
37 _LOGGER = logging.getLogger(__name__)
38 
39 
40 class SmartThingsFlowHandler(ConfigFlow, domain=DOMAIN):
41  """Handle configuration of SmartThings integrations."""
42 
43  VERSION = 2
44 
45  api: SmartThings
46  app_id: str
47  location_id: str
48 
49  def __init__(self) -> None:
50  """Create a new instance of the flow handler."""
51  self.access_tokenaccess_token: str | None = None
52  self.oauth_client_secretoauth_client_secret = None
53  self.oauth_client_idoauth_client_id = None
54  self.installed_app_idinstalled_app_id = None
55  self.refresh_tokenrefresh_token = None
56  self.endpoints_initializedendpoints_initialized = False
57 
58  async def async_step_import(self, import_data: None) -> ConfigFlowResult:
59  """Occurs when a previously entry setup fails and is re-initiated."""
60  return await self.async_step_userasync_step_userasync_step_user(import_data)
61 
62  async def async_step_user(
63  self, user_input: dict[str, Any] | None = None
64  ) -> ConfigFlowResult:
65  """Validate and confirm webhook setup."""
66  if not self.endpoints_initializedendpoints_initialized:
67  self.endpoints_initializedendpoints_initialized = True
69  self.hass, len(self._async_current_entries_async_current_entries()) == 0
70  )
71  webhook_url = get_webhook_url(self.hass)
72 
73  # Abort if the webhook is invalid
74  if not validate_webhook_requirements(self.hass):
75  return self.async_abortasync_abortasync_abort(
76  reason="invalid_webhook_url",
77  description_placeholders={
78  "webhook_url": webhook_url,
79  "component_url": (
80  "https://www.home-assistant.io/integrations/smartthings/"
81  ),
82  },
83  )
84 
85  # Show the confirmation
86  if user_input is None:
87  return self.async_show_formasync_show_formasync_show_form(
88  step_id="user",
89  description_placeholders={"webhook_url": webhook_url},
90  )
91 
92  # Show the next screen
93  return await self.async_step_patasync_step_pat()
94 
95  async def async_step_pat(
96  self, user_input: dict[str, str] | None = None
97  ) -> ConfigFlowResult:
98  """Get the Personal Access Token and validate it."""
99  errors: dict[str, str] = {}
100  if user_input is None or CONF_ACCESS_TOKEN not in user_input:
101  return self._show_step_pat_show_step_pat(errors)
102 
103  self.access_tokenaccess_token = user_input[CONF_ACCESS_TOKEN]
104 
105  # Ensure token is a UUID
106  if not VAL_UID_MATCHER.match(self.access_tokenaccess_token):
107  errors[CONF_ACCESS_TOKEN] = "token_invalid_format"
108  return self._show_step_pat_show_step_pat(errors)
109 
110  # Setup end-point
111  self.apiapi = SmartThings(async_get_clientsession(self.hass), self.access_tokenaccess_token)
112  try:
113  app = await find_app(self.hass, self.apiapi)
114  if app:
115  await app.refresh() # load all attributes
116  await update_app(self.hass, app)
117  # Find an existing entry to copy the oauth client
118  existing = next(
119  (
120  entry
121  for entry in self._async_current_entries_async_current_entries()
122  if entry.data[CONF_APP_ID] == app.app_id
123  ),
124  None,
125  )
126  if existing:
127  self.oauth_client_idoauth_client_id = existing.data[CONF_CLIENT_ID]
128  self.oauth_client_secretoauth_client_secret = existing.data[CONF_CLIENT_SECRET]
129  else:
130  # Get oauth client id/secret by regenerating it
131  app_oauth = AppOAuth(app.app_id)
132  app_oauth.client_name = APP_OAUTH_CLIENT_NAME
133  app_oauth.scope.extend(APP_OAUTH_SCOPES)
134  client = await self.apiapi.generate_app_oauth(app_oauth)
135  self.oauth_client_secretoauth_client_secret = client.client_secret
136  self.oauth_client_idoauth_client_id = client.client_id
137  else:
138  app, client = await create_app(self.hass, self.apiapi)
139  self.oauth_client_secretoauth_client_secret = client.client_secret
140  self.oauth_client_idoauth_client_id = client.client_id
141  setup_smartapp(self.hass, app)
142  self.app_idapp_id = app.app_id
143 
144  except APIResponseError as ex:
145  if ex.is_target_error():
146  errors["base"] = "webhook_error"
147  else:
148  errors["base"] = "app_setup_error"
149  _LOGGER.exception(
150  "API error setting up the SmartApp: %s", ex.raw_error_response
151  )
152  return self._show_step_pat_show_step_pat(errors)
153  except ClientResponseError as ex:
154  if ex.status == HTTPStatus.UNAUTHORIZED:
155  errors[CONF_ACCESS_TOKEN] = "token_unauthorized"
156  _LOGGER.debug(
157  "Unauthorized error received setting up SmartApp", exc_info=True
158  )
159  elif ex.status == HTTPStatus.FORBIDDEN:
160  errors[CONF_ACCESS_TOKEN] = "token_forbidden"
161  _LOGGER.debug(
162  "Forbidden error received setting up SmartApp", exc_info=True
163  )
164  else:
165  errors["base"] = "app_setup_error"
166  _LOGGER.exception("Unexpected error setting up the SmartApp")
167  return self._show_step_pat_show_step_pat(errors)
168  except Exception:
169  errors["base"] = "app_setup_error"
170  _LOGGER.exception("Unexpected error setting up the SmartApp")
171  return self._show_step_pat_show_step_pat(errors)
172 
173  return await self.async_step_select_locationasync_step_select_location()
174 
176  self, user_input: dict[str, str] | None = None
177  ) -> ConfigFlowResult:
178  """Ask user to select the location to setup."""
179  if user_input is None or CONF_LOCATION_ID not in user_input:
180  # Get available locations
181  existing_locations = [
182  entry.data[CONF_LOCATION_ID] for entry in self._async_current_entries_async_current_entries()
183  ]
184  locations = await self.apiapi.locations()
185  locations_options = {
186  location.location_id: location.name
187  for location in locations
188  if location.location_id not in existing_locations
189  }
190  if not locations_options:
191  return self.async_abortasync_abortasync_abort(reason="no_available_locations")
192 
193  return self.async_show_formasync_show_formasync_show_form(
194  step_id="select_location",
195  data_schema=vol.Schema(
196  {vol.Required(CONF_LOCATION_ID): vol.In(locations_options)}
197  ),
198  )
199 
200  self.location_idlocation_id = user_input[CONF_LOCATION_ID]
201  await self.async_set_unique_idasync_set_unique_id(format_unique_id(self.app_idapp_id, self.location_idlocation_id))
202  return await self.async_step_authorizeasync_step_authorize()
203 
205  self, user_input: dict[str, Any] | None = None
206  ) -> ConfigFlowResult:
207  """Wait for the user to authorize the app installation."""
208  user_input = {} if user_input is None else user_input
209  self.installed_app_idinstalled_app_id = user_input.get(CONF_INSTALLED_APP_ID)
210  self.refresh_tokenrefresh_token = user_input.get(CONF_REFRESH_TOKEN)
211  if self.installed_app_idinstalled_app_id is None:
212  # Launch the external setup URL
213  url = format_install_url(self.app_idapp_id, self.location_idlocation_id)
214  return self.async_external_stepasync_external_step(step_id="authorize", url=url)
215 
216  return self.async_external_step_doneasync_external_step_done(next_step_id="install")
217 
218  def _show_step_pat(self, errors):
219  if self.access_tokenaccess_token is None:
220  # Get the token from an existing entry to make it easier to setup multiple locations.
221  self.access_tokenaccess_token = next(
222  (
223  entry.data.get(CONF_ACCESS_TOKEN)
224  for entry in self._async_current_entries_async_current_entries()
225  ),
226  None,
227  )
228 
229  return self.async_show_formasync_show_formasync_show_form(
230  step_id="pat",
231  data_schema=vol.Schema(
232  {vol.Required(CONF_ACCESS_TOKEN, default=self.access_tokenaccess_token): str}
233  ),
234  errors=errors,
235  description_placeholders={
236  "token_url": "https://account.smartthings.com/tokens",
237  "component_url": (
238  "https://www.home-assistant.io/integrations/smartthings/"
239  ),
240  },
241  )
242 
244  self, user_input: dict[str, Any] | None = None
245  ) -> ConfigFlowResult:
246  """Create a config entry at completion of a flow and authorization of the app."""
247  data = {
248  CONF_ACCESS_TOKEN: self.access_tokenaccess_token,
249  CONF_REFRESH_TOKEN: self.refresh_tokenrefresh_token,
250  CONF_CLIENT_ID: self.oauth_client_idoauth_client_id,
251  CONF_CLIENT_SECRET: self.oauth_client_secretoauth_client_secret,
252  CONF_LOCATION_ID: self.location_idlocation_id,
253  CONF_APP_ID: self.app_idapp_id,
254  CONF_INSTALLED_APP_ID: self.installed_app_idinstalled_app_id,
255  }
256 
257  location = await self.apiapi.location(data[CONF_LOCATION_ID])
258 
259  return self.async_create_entryasync_create_entryasync_create_entry(title=location.name, data=data)
ConfigFlowResult async_step_select_location(self, dict[str, str]|None user_input=None)
Definition: config_flow.py:177
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:64
ConfigFlowResult async_step_install(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:245
ConfigFlowResult async_step_pat(self, dict[str, str]|None user_input=None)
Definition: config_flow.py:97
ConfigFlowResult async_step_authorize(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:206
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)
list[ConfigEntry] _async_current_entries(self, bool|None include_ignore=None)
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)
_FlowResultT async_external_step(self, *str|None step_id=None, str url, Mapping[str, str]|None description_placeholders=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_external_step_done(self, *str next_step_id)
_FlowResultT async_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
def setup_smartapp_endpoint(HomeAssistant hass, bool fresh_install)
Definition: smartapp.py:201
bool validate_webhook_requirements(HomeAssistant hass)
Definition: smartapp.py:97
AppEntity|None find_app(HomeAssistant hass, SmartThings api)
Definition: smartapp.py:67
str get_webhook_url(HomeAssistant hass)
Definition: smartapp.py:106
def create_app(HomeAssistant hass, api)
Definition: smartapp.py:139
def update_app(HomeAssistant hass, app)
Definition: smartapp.py:167
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)