Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for OctoPrint integration."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Mapping
7 import logging
8 from typing import TYPE_CHECKING, Any
9 
10 import aiohttp
11 from pyoctoprintapi import ApiError, OctoprintClient, OctoprintException
12 import voluptuous as vol
13 from yarl import URL
14 
15 from homeassistant.components import ssdp, zeroconf
16 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
17 from homeassistant.const import (
18  CONF_API_KEY,
19  CONF_HOST,
20  CONF_PATH,
21  CONF_PORT,
22  CONF_SSL,
23  CONF_USERNAME,
24  CONF_VERIFY_SSL,
25 )
26 from homeassistant.data_entry_flow import AbortFlow
27 from homeassistant.exceptions import HomeAssistantError
29 from homeassistant.util.ssl import get_default_context, get_default_no_verify_context
30 
31 from .const import DOMAIN
32 
33 _LOGGER = logging.getLogger(__name__)
34 
35 
37  username="", host="", port=80, path="/", ssl=False, verify_ssl=True
38 ):
39  return vol.Schema(
40  {
41  vol.Required(CONF_USERNAME, default=username): str,
42  vol.Required(CONF_HOST, default=host): str,
43  vol.Required(CONF_PORT, default=port): cv.port,
44  vol.Required(CONF_PATH, default=path): str,
45  vol.Required(CONF_SSL, default=ssl): bool,
46  vol.Required(CONF_VERIFY_SSL, default=verify_ssl): bool,
47  },
48  extra=vol.ALLOW_EXTRA,
49  )
50 
51 
52 class OctoPrintConfigFlow(ConfigFlow, domain=DOMAIN):
53  """Handle a config flow for OctoPrint."""
54 
55  VERSION = 1
56 
57  api_key_task: asyncio.Task[None] | None = None
58  discovery_schema: vol.Schema | None = None
59  _reauth_data: dict[str, Any] | None = None
60  _user_input: dict[str, Any] | None = None
61 
62  def __init__(self) -> None:
63  """Handle a config flow for OctoPrint."""
64  self._sessions: list[aiohttp.ClientSession] = []
65 
66  async def async_step_user(
67  self, user_input: dict[str, Any] | None = None
68  ) -> ConfigFlowResult:
69  """Handle the initial step."""
70  # When coming back from the progress steps, the user_input is stored in the
71  # instance variable instead of being passed in
72  if user_input is None and self._user_input_user_input:
73  user_input = self._user_input_user_input
74 
75  if user_input is None:
76  data = self.discovery_schemadiscovery_schema or _schema_with_defaults()
77  return self.async_show_formasync_show_formasync_show_form(step_id="user", data_schema=data)
78 
79  if CONF_API_KEY in user_input:
80  errors = {}
81  try:
82  return await self._finish_config_finish_config(user_input)
83  except AbortFlow as err:
84  raise err from None
85  except CannotConnect:
86  errors["base"] = "cannot_connect"
87  except Exception: # noqa: BLE001
88  errors["base"] = "unknown"
89 
90  if errors:
91  return self.async_show_formasync_show_formasync_show_form(
92  step_id="user",
93  errors=errors,
94  data_schema=_schema_with_defaults(
95  user_input.get(CONF_USERNAME),
96  user_input[CONF_HOST],
97  user_input[CONF_PORT],
98  user_input[CONF_PATH],
99  user_input[CONF_SSL],
100  user_input[CONF_VERIFY_SSL],
101  ),
102  )
103 
104  self._user_input_user_input = user_input
105  return await self.async_step_get_api_keyasync_step_get_api_key()
106 
108  self, user_input: dict[str, Any] | None = None
109  ) -> ConfigFlowResult:
110  """Get an Application Api Key."""
111  if not self.api_key_taskapi_key_task:
112  self.api_key_taskapi_key_task = self.hass.async_create_task(
113  self._async_get_auth_key_async_get_auth_key(), eager_start=False
114  )
115  if not self.api_key_taskapi_key_task.done():
116  return self.async_show_progressasync_show_progress(
117  step_id="get_api_key",
118  progress_action="get_api_key",
119  progress_task=self.api_key_taskapi_key_task,
120  )
121 
122  try:
123  await self.api_key_taskapi_key_task
124  except OctoprintException:
125  _LOGGER.exception("Failed to get an application key")
126  return self.async_show_progress_doneasync_show_progress_done(next_step_id="auth_failed")
127  except Exception:
128  _LOGGER.exception("Failed to get an application key")
129  return self.async_show_progress_doneasync_show_progress_done(next_step_id="auth_failed")
130  finally:
131  self.api_key_taskapi_key_task = None
132 
133  return self.async_show_progress_doneasync_show_progress_done(next_step_id="user")
134 
135  async def _finish_config(self, user_input: dict[str, Any]) -> ConfigFlowResult:
136  """Finish the configuration setup."""
137  existing_entry = await self.async_set_unique_idasync_set_unique_id(self.unique_idunique_id)
138  if existing_entry is not None:
139  self.hass.config_entries.async_update_entry(existing_entry, data=user_input)
140  # Reload the config entry otherwise devices will remain unavailable
141  self.hass.async_create_task(
142  self.hass.config_entries.async_reload(existing_entry.entry_id),
143  )
144 
145  return self.async_abortasync_abortasync_abort(reason="reauth_successful")
146 
147  octoprint = self._get_octoprint_client_get_octoprint_client(user_input)
148  octoprint.set_api_key(user_input[CONF_API_KEY])
149 
150  try:
151  discovery = await octoprint.get_discovery_info()
152  except ApiError as err:
153  _LOGGER.error("Failed to connect to printer")
154  raise CannotConnect from err
155 
156  await self.async_set_unique_idasync_set_unique_id(discovery.upnp_uuid, raise_on_progress=False)
157  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
158 
159  return self.async_create_entryasync_create_entryasync_create_entry(title=user_input[CONF_HOST], data=user_input)
160 
161  async def async_step_auth_failed(self, user_input: None) -> ConfigFlowResult:
162  """Handle api fetch failure."""
163  return self.async_abortasync_abortasync_abort(reason="auth_failed")
164 
165  async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
166  """Handle import."""
167  return await self.async_step_userasync_step_userasync_step_user(import_data)
168 
170  self, discovery_info: zeroconf.ZeroconfServiceInfo
171  ) -> ConfigFlowResult:
172  """Handle discovery flow."""
173  uuid = discovery_info.properties["uuid"]
174  await self.async_set_unique_idasync_set_unique_id(uuid)
175  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
176 
177  self.context.update(
178  {
179  "title_placeholders": {CONF_HOST: discovery_info.host},
180  "configuration_url": (
181  f"http://{discovery_info.host}:{discovery_info.port}"
182  f"{discovery_info.properties[CONF_PATH]}"
183  ),
184  }
185  )
186 
187  self.discovery_schemadiscovery_schema = _schema_with_defaults(
188  host=discovery_info.host,
189  port=discovery_info.port,
190  path=discovery_info.properties[CONF_PATH],
191  )
192 
193  return await self.async_step_userasync_step_userasync_step_user()
194 
195  async def async_step_ssdp(
196  self, discovery_info: ssdp.SsdpServiceInfo
197  ) -> ConfigFlowResult:
198  """Handle ssdp discovery flow."""
199  uuid = discovery_info.upnp["UDN"][5:]
200  await self.async_set_unique_idasync_set_unique_id(uuid)
201  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
202 
203  url = URL(discovery_info.upnp["presentationURL"])
204  self.context.update(
205  {
206  "title_placeholders": {CONF_HOST: url.host or "-"},
207  "configuration_url": discovery_info.upnp["presentationURL"],
208  }
209  )
210 
211  self.discovery_schemadiscovery_schema = _schema_with_defaults(
212  host=url.host,
213  path=url.path,
214  port=url.port,
215  ssl=url.scheme == "https",
216  )
217 
218  return await self.async_step_userasync_step_userasync_step_user()
219 
220  async def async_step_reauth(
221  self, entry_data: Mapping[str, Any]
222  ) -> ConfigFlowResult:
223  """Handle reauthorization request from Octoprint."""
224  self._reauth_data_reauth_data = dict(entry_data)
225 
226  self.context.update(
227  {
228  "title_placeholders": {CONF_HOST: entry_data[CONF_HOST]},
229  }
230  )
231 
232  return await self.async_step_reauth_confirmasync_step_reauth_confirm()
233 
235  self, user_input: dict[str, Any] | None = None
236  ) -> ConfigFlowResult:
237  """Handle reauthorization flow."""
238  assert self._reauth_data_reauth_data is not None
239 
240  if user_input is None:
241  return self.async_show_formasync_show_formasync_show_form(
242  step_id="reauth_confirm",
243  data_schema=vol.Schema(
244  {
245  vol.Required(
246  CONF_USERNAME, default=self._reauth_data_reauth_data[CONF_USERNAME]
247  ): str,
248  }
249  ),
250  )
251 
252  self._reauth_data_reauth_data[CONF_USERNAME] = user_input[CONF_USERNAME]
253 
254  self._user_input_user_input = self._reauth_data_reauth_data
255  return await self.async_step_get_api_keyasync_step_get_api_key()
256 
257  async def _async_get_auth_key(self) -> None:
258  """Get application api key."""
259  if TYPE_CHECKING:
260  assert self._user_input_user_input is not None
261  octoprint = self._get_octoprint_client_get_octoprint_client(self._user_input_user_input)
262 
263  self._user_input_user_input[CONF_API_KEY] = await octoprint.request_app_key(
264  "Home Assistant", self._user_input_user_input[CONF_USERNAME], 300
265  )
266 
267  def _get_octoprint_client(self, user_input: dict[str, Any]) -> OctoprintClient:
268  """Build an octoprint client from the user_input."""
269  verify_ssl = user_input.get(CONF_VERIFY_SSL, True)
270 
271  connector = aiohttp.TCPConnector(
272  force_close=True,
274  if not verify_ssl
275  else get_default_context(),
276  )
277  session = aiohttp.ClientSession(connector=connector)
278  self._sessions.append(session)
279 
280  return OctoprintClient(
281  host=user_input[CONF_HOST],
282  session=session,
283  port=user_input[CONF_PORT],
284  ssl=user_input[CONF_SSL],
285  path=user_input[CONF_PATH],
286  )
287 
288  def async_remove(self) -> None:
289  """Detach the session."""
290  for session in self._sessions:
291  session.detach()
292 
293 
295  """Error to indicate we cannot connect."""
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:222
ConfigFlowResult async_step_get_api_key(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:109
ConfigFlowResult async_step_ssdp(self, ssdp.SsdpServiceInfo discovery_info)
Definition: config_flow.py:197
ConfigFlowResult _finish_config(self, dict[str, Any] user_input)
Definition: config_flow.py:135
ConfigFlowResult async_step_import(self, dict[str, Any] import_data)
Definition: config_flow.py:165
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:236
ConfigFlowResult async_step_auth_failed(self, None user_input)
Definition: config_flow.py:161
OctoprintClient _get_octoprint_client(self, dict[str, Any] user_input)
Definition: config_flow.py:267
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:68
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:171
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_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_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_progress(self, *str|None step_id=None, str progress_action, Mapping[str, str]|None description_placeholders=None, asyncio.Task[Any]|None progress_task=None)
_FlowResultT async_show_progress_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)
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
def _schema_with_defaults(username="", host="", port=80, path="/", ssl=False, verify_ssl=True)
Definition: config_flow.py:38
ssl.SSLContext get_default_context()
Definition: ssl.py:118
ssl.SSLContext get_default_no_verify_context()
Definition: ssl.py:123