Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for System Bridge integration."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Mapping
7 import logging
8 from typing import Any
9 
10 from systembridgeconnector.exceptions import (
11  AuthenticationException,
12  ConnectionClosedException,
13  ConnectionErrorException,
14 )
15 from systembridgeconnector.websocket_client import WebSocketClient
16 from systembridgemodels.modules import GetData, Module
17 import voluptuous as vol
18 
19 from homeassistant.components import zeroconf
20 from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
21 from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN
22 from homeassistant.core import HomeAssistant
23 from homeassistant.exceptions import HomeAssistantError
24 from homeassistant.helpers import config_validation as cv
25 from homeassistant.helpers.aiohttp_client import async_get_clientsession
26 
27 from .const import DATA_WAIT_TIMEOUT, DOMAIN
28 
29 _LOGGER = logging.getLogger(__name__)
30 
31 STEP_AUTHENTICATE_DATA_SCHEMA = vol.Schema({vol.Required(CONF_TOKEN): cv.string})
32 STEP_USER_DATA_SCHEMA = vol.Schema(
33  {
34  vol.Required(CONF_HOST): cv.string,
35  vol.Required(CONF_PORT, default=9170): cv.string,
36  vol.Required(CONF_TOKEN): cv.string,
37  }
38 )
39 
40 
41 async def _validate_input(
42  hass: HomeAssistant,
43  data: dict[str, Any],
44 ) -> dict[str, str]:
45  """Validate the user input allows us to connect.
46 
47  Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
48  """
49 
50  websocket_client = WebSocketClient(
51  data[CONF_HOST],
52  data[CONF_PORT],
53  data[CONF_TOKEN],
54  session=async_get_clientsession(hass),
55  )
56 
57  try:
58  async with asyncio.timeout(DATA_WAIT_TIMEOUT):
59  await websocket_client.connect()
60  modules_data = await websocket_client.get_data(
61  GetData(modules=[Module.SYSTEM])
62  )
63  except AuthenticationException as exception:
64  _LOGGER.warning(
65  "Authentication error when connecting to %s: %s",
66  data[CONF_HOST],
67  exception,
68  )
69  raise InvalidAuth from exception
70  except (
71  ConnectionClosedException,
72  ConnectionErrorException,
73  ) as exception:
74  _LOGGER.warning(
75  "Connection error when connecting to %s: %s", data[CONF_HOST], exception
76  )
77  raise CannotConnect from exception
78  except TimeoutError as exception:
79  _LOGGER.warning("Timed out connecting to %s: %s", data[CONF_HOST], exception)
80  raise CannotConnect from exception
81  except ValueError as exception:
82  raise CannotConnect from exception
83 
84  _LOGGER.debug("Got modules data: %s", modules_data)
85  if modules_data is None or modules_data.system is None:
86  raise CannotConnect("No system data received")
87 
88  _LOGGER.debug("Got System data: %s", modules_data.system)
89 
90  return {"hostname": data[CONF_HOST], "uuid": modules_data.system.uuid}
91 
92 
93 async def _async_get_info(
94  hass: HomeAssistant,
95  user_input: dict[str, Any],
96 ) -> tuple[dict[str, str], dict[str, str] | None]:
97  errors = {}
98 
99  try:
100  info = await _validate_input(hass, user_input)
101  except CannotConnect:
102  errors["base"] = "cannot_connect"
103  except InvalidAuth:
104  errors["base"] = "invalid_auth"
105  except Exception:
106  _LOGGER.exception("Unexpected exception")
107  errors["base"] = "unknown"
108  else:
109  return errors, info
110 
111  return errors, None
112 
113 
115  ConfigFlow,
116  domain=DOMAIN,
117 ):
118  """Handle a config flow for System Bridge."""
119 
120  VERSION = 1
121  MINOR_VERSION = 2
122 
123  _name: str
124 
125  def __init__(self) -> None:
126  """Initialize flow."""
127  self._input_input: dict[str, Any] = {}
128 
129  async def async_step_user(
130  self, user_input: dict[str, Any] | None = None
131  ) -> ConfigFlowResult:
132  """Handle the initial step."""
133  if user_input is None:
134  return self.async_show_formasync_show_formasync_show_form(
135  step_id="user", data_schema=STEP_USER_DATA_SCHEMA
136  )
137 
138  errors, info = await _async_get_info(self.hass, user_input)
139  if not errors and info is not None:
140  # Check if already configured
141  await self.async_set_unique_idasync_set_unique_id(info["uuid"], raise_on_progress=False)
142  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_HOST: info["hostname"]})
143 
144  return self.async_create_entryasync_create_entryasync_create_entry(title=info["hostname"], data=user_input)
145 
146  return self.async_show_formasync_show_formasync_show_form(
147  step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
148  )
149 
151  self, user_input: dict[str, Any] | None = None
152  ) -> ConfigFlowResult:
153  """Handle getting the api-key for authentication."""
154  errors: dict[str, str] = {}
155 
156  if user_input is not None:
157  user_input = {**self._input_input, **user_input}
158  errors, info = await _async_get_info(self.hass, user_input)
159  if not errors and info is not None:
160  await self.async_set_unique_idasync_set_unique_id(info["uuid"])
161 
162  if self.sourcesourcesourcesource == SOURCE_REAUTH:
163  self._abort_if_unique_id_mismatch_abort_if_unique_id_mismatch()
164  return self.async_update_reload_and_abortasync_update_reload_and_abort(
165  self._get_reauth_entry_get_reauth_entry(), data=user_input
166  )
167 
168  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
169  updates={CONF_HOST: info["hostname"]}
170  )
171 
172  return self.async_create_entryasync_create_entryasync_create_entry(title=info["hostname"], data=user_input)
173 
174  return self.async_show_formasync_show_formasync_show_form(
175  step_id="authenticate",
176  data_schema=STEP_AUTHENTICATE_DATA_SCHEMA,
177  description_placeholders={"name": self._name_name},
178  errors=errors,
179  )
180 
182  self, discovery_info: zeroconf.ZeroconfServiceInfo
183  ) -> ConfigFlowResult:
184  """Handle zeroconf discovery."""
185  properties = discovery_info.properties
186  host = properties.get("ip")
187  uuid = properties.get("uuid")
188 
189  if host is None or uuid is None:
190  return self.async_abortasync_abortasync_abort(reason="unknown")
191 
192  # Check if already configured
193  await self.async_set_unique_idasync_set_unique_id(uuid)
194  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_HOST: host})
195 
196  self._name_name = host
197  self._input_input = {
198  CONF_HOST: host,
199  CONF_PORT: properties.get("port"),
200  }
201 
202  return await self.async_step_authenticateasync_step_authenticate()
203 
204  async def async_step_reauth(
205  self, entry_data: Mapping[str, Any]
206  ) -> ConfigFlowResult:
207  """Perform reauth upon an API authentication error."""
208  self._name_name = entry_data[CONF_HOST]
209  self._input_input = {
210  CONF_HOST: entry_data[CONF_HOST],
211  CONF_PORT: entry_data[CONF_PORT],
212  }
213  return await self.async_step_authenticateasync_step_authenticate()
214 
215 
217  """Error to indicate we cannot connect."""
218 
219 
220 class InvalidAuth(HomeAssistantError):
221  """Error to indicate there is invalid auth."""
ConfigFlowResult async_step_authenticate(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:152
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:206
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:183
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:131
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_update_reload_and_abort(self, ConfigEntry entry, *str|None|UndefinedType unique_id=UNDEFINED, str|UndefinedType title=UNDEFINED, Mapping[str, Any]|UndefinedType data=UNDEFINED, Mapping[str, Any]|UndefinedType data_updates=UNDEFINED, Mapping[str, Any]|UndefinedType options=UNDEFINED, str|UndefinedType reason=UNDEFINED, bool reload_even_if_entry_is_unchanged=True)
ConfigFlowResult async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
None _abort_if_unique_id_mismatch(self, *str reason="unique_id_mismatch", 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_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
str|None source(self)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
tuple[dict[str, str], dict[str, str]|None] _async_get_info(HomeAssistant hass, dict[str, Any] user_input)
Definition: config_flow.py:96
dict[str, str] _validate_input(HomeAssistant hass, dict[str, Any] data)
Definition: config_flow.py:44
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)