Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Kodi integration."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 
8 from pykodi import CannotConnectError, InvalidAuthError, Kodi, get_kodi_connection
9 import voluptuous as vol
10 
11 from homeassistant.components import zeroconf
12 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
13 from homeassistant.const import (
14  CONF_HOST,
15  CONF_NAME,
16  CONF_PASSWORD,
17  CONF_PORT,
18  CONF_SSL,
19  CONF_TIMEOUT,
20  CONF_USERNAME,
21 )
22 from homeassistant.core import HomeAssistant, callback
23 from homeassistant.exceptions import HomeAssistantError
24 from homeassistant.helpers.aiohttp_client import async_get_clientsession
25 
26 from .const import (
27  CONF_WS_PORT,
28  DEFAULT_PORT,
29  DEFAULT_SSL,
30  DEFAULT_TIMEOUT,
31  DEFAULT_WS_PORT,
32  DOMAIN,
33 )
34 
35 _LOGGER = logging.getLogger(__name__)
36 
37 
38 async def validate_http(hass: HomeAssistant, data):
39  """Validate the user input allows us to connect over HTTP."""
40 
41  host = data[CONF_HOST]
42  port = data[CONF_PORT]
43  username = data.get(CONF_USERNAME)
44  password = data.get(CONF_PASSWORD)
45  ssl = data.get(CONF_SSL)
46  session = async_get_clientsession(hass)
47 
48  _LOGGER.debug("Connecting to %s:%s over HTTP", host, port)
49  khc = get_kodi_connection(
50  host, port, None, username, password, ssl, session=session
51  )
52  kodi = Kodi(khc)
53  try:
54  await kodi.ping()
55  except CannotConnectError as error:
56  raise CannotConnect from error
57  except InvalidAuthError as error:
58  raise InvalidAuth from error
59 
60 
61 async def validate_ws(hass: HomeAssistant, data):
62  """Validate the user input allows us to connect over WS."""
63  if not (ws_port := data.get(CONF_WS_PORT)):
64  return
65 
66  host = data[CONF_HOST]
67  port = data[CONF_PORT]
68  username = data.get(CONF_USERNAME)
69  password = data.get(CONF_PASSWORD)
70  ssl = data.get(CONF_SSL)
71 
72  session = async_get_clientsession(hass)
73 
74  _LOGGER.debug("Connecting to %s:%s over WebSocket", host, ws_port)
75  kwc = get_kodi_connection(
76  host, port, ws_port, username, password, ssl, session=session
77  )
78  try:
79  await kwc.connect()
80  if not kwc.connected:
81  _LOGGER.warning("Cannot connect to %s:%s over WebSocket", host, ws_port)
82  raise WSCannotConnect
83  kodi = Kodi(kwc)
84  await kodi.ping()
85  except CannotConnectError as error:
86  raise WSCannotConnect from error
87 
88 
89 class KodiConfigFlow(ConfigFlow, domain=DOMAIN):
90  """Handle a config flow for Kodi."""
91 
92  VERSION = 1
93 
94  def __init__(self) -> None:
95  """Initialize flow."""
96  self._host_host: str | None = None
97  self._port_port: int | None = DEFAULT_PORT
98  self._ws_port_ws_port: int | None = DEFAULT_WS_PORT
99  self._name_name: str | None = None
100  self._username_username: str | None = None
101  self._password_password: str | None = None
102  self._ssl_ssl: bool | None = DEFAULT_SSL
103  self._discovery_name_discovery_name: str | None = None
104 
106  self, discovery_info: zeroconf.ZeroconfServiceInfo
107  ) -> ConfigFlowResult:
108  """Handle zeroconf discovery."""
109  self._host_host = discovery_info.host
110  self._port_port = discovery_info.port or DEFAULT_PORT
111  self._name_name = discovery_info.hostname.removesuffix(".local.")
112  if not (uuid := discovery_info.properties.get("uuid")):
113  return self.async_abortasync_abortasync_abort(reason="no_uuid")
114 
115  self._discovery_name_discovery_name = discovery_info.name
116 
117  await self.async_set_unique_idasync_set_unique_id(uuid)
118  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
119  updates={
120  CONF_HOST: self._host_host,
121  CONF_PORT: self._port_port,
122  CONF_NAME: self._name_name,
123  }
124  )
125 
126  self.context.update({"title_placeholders": {CONF_NAME: self._name_name}})
127 
128  try:
129  await validate_http(self.hass, self._get_data_get_data())
130  await validate_ws(self.hass, self._get_data_get_data())
131  except InvalidAuth:
132  return await self.async_step_credentialsasync_step_credentials()
133  except WSCannotConnect:
134  return await self.async_step_ws_portasync_step_ws_port()
135  except CannotConnect:
136  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
137  except Exception:
138  _LOGGER.exception("Unexpected exception")
139  return self.async_abortasync_abortasync_abort(reason="unknown")
140 
141  return await self.async_step_discovery_confirmasync_step_discovery_confirm()
142 
144  self, user_input: dict[str, Any] | None = None
145  ) -> ConfigFlowResult:
146  """Handle user-confirmation of discovered node."""
147  if user_input is None:
148  assert self._name_name is not None
149  return self.async_show_formasync_show_formasync_show_form(
150  step_id="discovery_confirm",
151  description_placeholders={"name": self._name_name},
152  )
153 
154  return self._create_entry_create_entry()
155 
156  async def async_step_user(
157  self, user_input: dict[str, Any] | None = None
158  ) -> ConfigFlowResult:
159  """Handle the initial step."""
160  errors = {}
161 
162  if user_input is not None:
163  self._host_host = user_input[CONF_HOST]
164  self._port_port = user_input[CONF_PORT]
165  self._ssl_ssl = user_input[CONF_SSL]
166 
167  try:
168  await validate_http(self.hass, self._get_data_get_data())
169  await validate_ws(self.hass, self._get_data_get_data())
170  except InvalidAuth:
171  return await self.async_step_credentialsasync_step_credentials()
172  except WSCannotConnect:
173  return await self.async_step_ws_portasync_step_ws_port()
174  except CannotConnect:
175  errors["base"] = "cannot_connect"
176  except Exception:
177  _LOGGER.exception("Unexpected exception")
178  errors["base"] = "unknown"
179  else:
180  return self._create_entry_create_entry()
181 
182  return self._show_user_form_show_user_form(errors)
183 
185  self, user_input: dict[str, Any] | None = None
186  ) -> ConfigFlowResult:
187  """Handle username and password input."""
188  errors = {}
189 
190  if user_input is not None:
191  self._username_username = user_input.get(CONF_USERNAME)
192  self._password_password = user_input.get(CONF_PASSWORD)
193 
194  try:
195  await validate_http(self.hass, self._get_data_get_data())
196  await validate_ws(self.hass, self._get_data_get_data())
197  except InvalidAuth:
198  errors["base"] = "invalid_auth"
199  except WSCannotConnect:
200  return await self.async_step_ws_portasync_step_ws_port()
201  except CannotConnect:
202  errors["base"] = "cannot_connect"
203  except Exception:
204  _LOGGER.exception("Unexpected exception")
205  errors["base"] = "unknown"
206  else:
207  return self._create_entry_create_entry()
208 
209  return self._show_credentials_form_show_credentials_form(errors)
210 
212  self, user_input: dict[str, Any] | None = None
213  ) -> ConfigFlowResult:
214  """Handle websocket port of discovered node."""
215  errors = {}
216 
217  if user_input is not None:
218  self._ws_port_ws_port = user_input.get(CONF_WS_PORT)
219 
220  # optional ints return 0 rather than None when empty
221  if self._ws_port_ws_port == 0:
222  self._ws_port_ws_port = None
223 
224  try:
225  await validate_ws(self.hass, self._get_data_get_data())
226  except WSCannotConnect:
227  errors["base"] = "cannot_connect"
228  except Exception:
229  _LOGGER.exception("Unexpected exception")
230  errors["base"] = "unknown"
231  else:
232  return self._create_entry_create_entry()
233 
234  return self._show_ws_port_form_show_ws_port_form(errors)
235 
236  async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
237  """Handle import from YAML."""
238  reason = None
239  try:
240  await validate_http(self.hass, import_data)
241  await validate_ws(self.hass, import_data)
242  except InvalidAuth:
243  _LOGGER.exception("Invalid Kodi credentials")
244  reason = "invalid_auth"
245  except CannotConnect:
246  _LOGGER.exception("Cannot connect to Kodi")
247  reason = "cannot_connect"
248  except Exception:
249  _LOGGER.exception("Unexpected exception")
250  reason = "unknown"
251  else:
252  return self.async_create_entryasync_create_entryasync_create_entry(
253  title=import_data[CONF_NAME], data=import_data
254  )
255 
256  return self.async_abortasync_abortasync_abort(reason=reason)
257 
258  @callback
260  self, errors: dict[str, str] | None = None
261  ) -> ConfigFlowResult:
262  schema = vol.Schema(
263  {
264  vol.Optional(
265  CONF_USERNAME, description={"suggested_value": self._username_username}
266  ): str,
267  vol.Optional(
268  CONF_PASSWORD, description={"suggested_value": self._password_password}
269  ): str,
270  }
271  )
272 
273  return self.async_show_formasync_show_formasync_show_form(
274  step_id="credentials", data_schema=schema, errors=errors
275  )
276 
277  @callback
278  def _show_user_form(self, errors=None):
279  default_port = self._port_port or DEFAULT_PORT
280  default_ssl = self._ssl_ssl or DEFAULT_SSL
281  schema = vol.Schema(
282  {
283  vol.Required(CONF_HOST, default=self._host_host): str,
284  vol.Required(CONF_PORT, default=default_port): int,
285  vol.Required(CONF_SSL, default=default_ssl): bool,
286  }
287  )
288 
289  return self.async_show_formasync_show_formasync_show_form(
290  step_id="user", data_schema=schema, errors=errors or {}
291  )
292 
293  @callback
294  def _show_ws_port_form(self, errors=None):
295  suggestion = self._ws_port_ws_port or DEFAULT_WS_PORT
296  schema = vol.Schema(
297  {
298  vol.Optional(
299  CONF_WS_PORT, description={"suggested_value": suggestion}
300  ): int
301  }
302  )
303 
304  return self.async_show_formasync_show_formasync_show_form(
305  step_id="ws_port", data_schema=schema, errors=errors or {}
306  )
307 
308  @callback
309  def _create_entry(self):
310  return self.async_create_entryasync_create_entryasync_create_entry(
311  title=self._name_name or self._host_host,
312  data=self._get_data_get_data(),
313  )
314 
315  @callback
316  def _get_data(self) -> dict[str, Any]:
317  return {
318  CONF_NAME: self._name_name,
319  CONF_HOST: self._host_host,
320  CONF_PORT: self._port_port,
321  CONF_WS_PORT: self._ws_port_ws_port,
322  CONF_USERNAME: self._username_username,
323  CONF_PASSWORD: self._password_password,
324  CONF_SSL: self._ssl_ssl,
325  CONF_TIMEOUT: DEFAULT_TIMEOUT,
326  }
327 
328 
330  """Error to indicate we cannot connect."""
331 
332 
333 class InvalidAuth(HomeAssistantError):
334  """Error to indicate there is invalid auth."""
335 
336 
338  """Error to indicate we cannot connect to websocket."""
ConfigFlowResult async_step_ws_port(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:213
ConfigFlowResult _show_credentials_form(self, dict[str, str]|None errors=None)
Definition: config_flow.py:261
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:107
ConfigFlowResult async_step_credentials(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:186
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:158
ConfigFlowResult async_step_discovery_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:145
ConfigFlowResult async_step_import(self, dict[str, Any] import_data)
Definition: config_flow.py:236
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_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_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 validate_http(HomeAssistant hass, data)
Definition: config_flow.py:38
def validate_ws(HomeAssistant hass, data)
Definition: config_flow.py:61
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)