Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for BleBox devices integration."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 
8 from blebox_uniapi.box import Box
9 from blebox_uniapi.error import (
10  Error,
11  UnauthorizedRequest,
12  UnsupportedBoxResponse,
13  UnsupportedBoxVersion,
14 )
15 from blebox_uniapi.session import ApiHost
16 import voluptuous as vol
17 
18 from homeassistant.components import zeroconf
19 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
20 from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
21 from homeassistant.helpers.aiohttp_client import async_get_clientsession
22 
23 from . import get_maybe_authenticated_session
24 from .const import (
25  ADDRESS_ALREADY_CONFIGURED,
26  CANNOT_CONNECT,
27  DEFAULT_HOST,
28  DEFAULT_PORT,
29  DEFAULT_SETUP_TIMEOUT,
30  DOMAIN,
31  UNKNOWN,
32  UNSUPPORTED_VERSION,
33 )
34 
35 _LOGGER = logging.getLogger(__name__)
36 
37 
38 def create_schema(previous_input=None):
39  """Create a schema with given values as default."""
40  if previous_input is not None:
41  host = previous_input[CONF_HOST]
42  port = previous_input[CONF_PORT]
43  else:
44  host = DEFAULT_HOST
45  port = DEFAULT_PORT
46 
47  return vol.Schema(
48  {
49  vol.Required(CONF_HOST, default=host): str,
50  vol.Required(CONF_PORT, default=port): int,
51  vol.Inclusive(CONF_USERNAME, "auth"): str,
52  vol.Inclusive(CONF_PASSWORD, "auth"): str,
53  }
54  )
55 
56 
57 LOG_MSG = {
58  UNSUPPORTED_VERSION: "Outdated firmware",
59  CANNOT_CONNECT: "Failed to identify device",
60  UNKNOWN: "Unknown error while identifying device",
61 }
62 
63 
64 class BleBoxConfigFlow(ConfigFlow, domain=DOMAIN):
65  """Handle a config flow for BleBox devices."""
66 
67  VERSION = 1
68 
69  def __init__(self) -> None:
70  """Initialize the BleBox config flow."""
71  self.device_config: dict[str, Any] = {}
72 
74  self, step, exception, schema, host, port, message_id, log_fn
75  ):
76  """Handle step exceptions."""
77  log_fn("%s at %s:%d (%s)", LOG_MSG[message_id], host, port, exception)
78 
79  return self.async_show_formasync_show_formasync_show_form(
80  step_id="user",
81  data_schema=schema,
82  errors={"base": message_id},
83  description_placeholders={"address": f"{host}:{port}"},
84  )
85 
87  self, discovery_info: zeroconf.ZeroconfServiceInfo
88  ) -> ConfigFlowResult:
89  """Handle zeroconf discovery."""
90  hass = self.hass
91  ipaddress = (discovery_info.host, discovery_info.port)
92  self.device_config["host"] = discovery_info.host
93  self.device_config["port"] = discovery_info.port
94 
95  websession = async_get_clientsession(hass)
96 
97  api_host = ApiHost(
98  *ipaddress, DEFAULT_SETUP_TIMEOUT, websession, hass.loop, _LOGGER
99  )
100 
101  try:
102  product = await Box.async_from_host(api_host)
103  except UnsupportedBoxVersion:
104  return self.async_abortasync_abortasync_abort(reason="unsupported_device_version")
105  except UnsupportedBoxResponse:
106  return self.async_abortasync_abortasync_abort(reason="unsupported_device_response")
107 
108  self.device_config["name"] = product.name
109  # Check if configured but IP changed since
110  await self.async_set_unique_idasync_set_unique_id(product.unique_id)
111  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.host})
112  self.context.update(
113  {
114  "title_placeholders": {
115  "name": self.device_config["name"],
116  "host": self.device_config["host"],
117  },
118  "configuration_url": f"http://{discovery_info.host}",
119  }
120  )
121  return await self.async_step_confirm_discoveryasync_step_confirm_discovery()
122 
124  self, user_input: dict[str, Any] | None = None
125  ) -> ConfigFlowResult:
126  """Handle discovery confirmation."""
127  if user_input is not None:
128  return self.async_create_entryasync_create_entryasync_create_entry(
129  title=self.device_config["name"],
130  data={
131  "host": self.device_config["host"],
132  "port": self.device_config["port"],
133  },
134  )
135 
136  return self.async_show_formasync_show_formasync_show_form(
137  step_id="confirm_discovery",
138  description_placeholders={
139  "name": self.device_config["name"],
140  "host": self.device_config["host"],
141  "port": self.device_config["port"],
142  },
143  )
144 
145  async def async_step_user(
146  self, user_input: dict[str, Any] | None = None
147  ) -> ConfigFlowResult:
148  """Handle initial user-triggered config step."""
149  hass = self.hass
150  schema = create_schema(user_input)
151 
152  if user_input is None:
153  return self.async_show_formasync_show_formasync_show_form(
154  step_id="user",
155  data_schema=schema,
156  errors={},
157  description_placeholders={},
158  )
159 
160  host = user_input[CONF_HOST]
161  port = user_input[CONF_PORT]
162 
163  username = user_input.get(CONF_USERNAME)
164  password = user_input.get(CONF_PASSWORD)
165 
166  for entry in self._async_current_entries_async_current_entries():
167  if host == entry.data[CONF_HOST] and port == entry.data[CONF_PORT]:
168  return self.async_abortasync_abortasync_abort(
169  reason=ADDRESS_ALREADY_CONFIGURED,
170  description_placeholders={"address": f"{host}:{port}"},
171  )
172 
173  websession = get_maybe_authenticated_session(hass, password, username)
174 
175  api_host = ApiHost(
176  host, port, DEFAULT_SETUP_TIMEOUT, websession, hass.loop, _LOGGER
177  )
178  try:
179  product = await Box.async_from_host(api_host)
180 
181  except UnsupportedBoxVersion as ex:
182  return self.handle_step_exceptionhandle_step_exception(
183  "user",
184  ex,
185  schema,
186  host,
187  port,
188  UNSUPPORTED_VERSION,
189  _LOGGER.debug,
190  )
191  except UnauthorizedRequest as ex:
192  return self.handle_step_exceptionhandle_step_exception(
193  "user", ex, schema, host, port, CANNOT_CONNECT, _LOGGER.error
194  )
195 
196  except Error as ex:
197  return self.handle_step_exceptionhandle_step_exception(
198  "user", ex, schema, host, port, CANNOT_CONNECT, _LOGGER.warning
199  )
200 
201  except RuntimeError as ex:
202  return self.handle_step_exceptionhandle_step_exception(
203  "user", ex, schema, host, port, UNKNOWN, _LOGGER.error
204  )
205 
206  # Check if configured but IP changed since
207  await self.async_set_unique_idasync_set_unique_id(product.unique_id, raise_on_progress=False)
208  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
209 
210  return self.async_create_entryasync_create_entryasync_create_entry(title=product.name, data=user_input)
ConfigFlowResult async_step_confirm_discovery(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:125
def handle_step_exception(self, step, exception, schema, host, port, message_id, log_fn)
Definition: config_flow.py:75
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:147
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:88
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)
list[ConfigEntry] _async_current_entries(self, bool|None include_ignore=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)
def create_schema(previous_input=None)
Definition: config_flow.py:38
aiohttp.ClientSession get_maybe_authenticated_session(HomeAssistant hass, str|None password, str|None username)
Definition: helpers.py:16
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
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)