Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for DoorBird integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 from http import HTTPStatus
7 import logging
8 from typing import Any
9 
10 from aiohttp import ClientResponseError
11 from doorbirdpy import DoorBird
12 import voluptuous as vol
13 
14 from homeassistant.components import zeroconf
15 from homeassistant.config_entries import (
16  ConfigEntry,
17  ConfigFlow,
18  ConfigFlowResult,
19  OptionsFlow,
20 )
21 from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
22 from homeassistant.core import HomeAssistant, callback
23 from homeassistant.exceptions import HomeAssistantError
24 from homeassistant.helpers.aiohttp_client import async_get_clientsession
25 from homeassistant.helpers.typing import VolDictType
26 
27 from .const import (
28  CONF_EVENTS,
29  DEFAULT_DOORBELL_EVENT,
30  DEFAULT_MOTION_EVENT,
31  DOMAIN,
32  DOORBIRD_OUI,
33 )
34 from .util import get_mac_address_from_door_station_info
35 
36 _LOGGER = logging.getLogger(__name__)
37 
38 DEFAULT_OPTIONS = {CONF_EVENTS: [DEFAULT_DOORBELL_EVENT, DEFAULT_MOTION_EVENT]}
39 
40 
41 AUTH_VOL_DICT: VolDictType = {
42  vol.Required(CONF_USERNAME): str,
43  vol.Required(CONF_PASSWORD): str,
44 }
45 AUTH_SCHEMA = vol.Schema(AUTH_VOL_DICT)
46 
47 
49  host: str | None = None, name: str | None = None
50 ) -> vol.Schema:
51  return vol.Schema(
52  {
53  vol.Required(CONF_HOST, default=host): str,
54  **AUTH_VOL_DICT,
55  vol.Optional(CONF_NAME, default=name): str,
56  }
57  )
58 
59 
60 async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]:
61  """Validate the user input allows us to connect."""
62  session = async_get_clientsession(hass)
63  device = DoorBird(
64  data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD], http_session=session
65  )
66  try:
67  info = await device.info()
68  except ClientResponseError as err:
69  if err.status == HTTPStatus.UNAUTHORIZED:
70  raise InvalidAuth from err
71  raise CannotConnect from err
72  except OSError as err:
73  raise CannotConnect from err
74 
76 
77  # Return info that you want to store in the config entry.
78  return {"title": data[CONF_HOST], "mac_addr": mac_addr}
79 
80 
81 async def async_verify_supported_device(hass: HomeAssistant, host: str) -> bool:
82  """Verify the doorbell state endpoint returns a 401."""
83  session = async_get_clientsession(hass)
84  device = DoorBird(host, "", "", http_session=session)
85  try:
86  await device.doorbell_state()
87  except ClientResponseError as err:
88  if err.status == HTTPStatus.UNAUTHORIZED:
89  return True
90  except OSError:
91  return False
92  return False
93 
94 
95 class DoorBirdConfigFlow(ConfigFlow, domain=DOMAIN):
96  """Handle a config flow for DoorBird."""
97 
98  VERSION = 1
99 
100  reauth_entry: ConfigEntry
101 
102  def __init__(self) -> None:
103  """Initialize the DoorBird config flow."""
104  self.discovery_schemadiscovery_schema: vol.Schema | None = None
105 
106  async def async_step_reauth(
107  self, entry_data: Mapping[str, Any]
108  ) -> ConfigFlowResult:
109  """Handle reauth."""
110  self.reauth_entryreauth_entry = self._get_reauth_entry_get_reauth_entry()
111  return await self.async_step_reauth_confirmasync_step_reauth_confirm()
112 
114  self, user_input: dict[str, Any] | None = None
115  ) -> ConfigFlowResult:
116  """Handle reauth input."""
117  errors: dict[str, str] = {}
118  existing_data = self.reauth_entryreauth_entry.data
119  placeholders: dict[str, str] = {
120  CONF_NAME: existing_data[CONF_NAME],
121  CONF_HOST: existing_data[CONF_HOST],
122  }
123  self.context["title_placeholders"] = placeholders
124  if user_input is not None:
125  new_config = {
126  **existing_data,
127  CONF_USERNAME: user_input[CONF_USERNAME],
128  CONF_PASSWORD: user_input[CONF_PASSWORD],
129  }
130  _, errors = await self._async_validate_or_error_async_validate_or_error(new_config)
131  if not errors:
132  return self.async_update_reload_and_abortasync_update_reload_and_abort(
133  self.reauth_entryreauth_entry, data=new_config
134  )
135 
136  return self.async_show_formasync_show_formasync_show_form(
137  description_placeholders=placeholders,
138  step_id="reauth_confirm",
139  data_schema=AUTH_SCHEMA,
140  errors=errors,
141  )
142 
143  async def async_step_user(
144  self, user_input: dict[str, Any] | None = None
145  ) -> ConfigFlowResult:
146  """Handle the initial step."""
147  errors: dict[str, str] = {}
148  if user_input is not None:
149  info, errors = await self._async_validate_or_error_async_validate_or_error(user_input)
150  if not errors:
151  await self.async_set_unique_idasync_set_unique_id(info["mac_addr"])
152  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
153  return self.async_create_entryasync_create_entryasync_create_entry(
154  title=info["title"], data=user_input, options=DEFAULT_OPTIONS
155  )
156 
157  data = self.discovery_schemadiscovery_schema or _schema_with_defaults()
158  return self.async_show_formasync_show_formasync_show_form(step_id="user", data_schema=data, errors=errors)
159 
161  self, discovery_info: zeroconf.ZeroconfServiceInfo
162  ) -> ConfigFlowResult:
163  """Prepare configuration for a discovered doorbird device."""
164  macaddress = discovery_info.properties["macaddress"]
165 
166  if macaddress[:6] != DOORBIRD_OUI:
167  return self.async_abortasync_abortasync_abort(reason="not_doorbird_device")
168  if discovery_info.ip_address.is_link_local:
169  return self.async_abortasync_abortasync_abort(reason="link_local_address")
170  if discovery_info.ip_address.version != 4:
171  return self.async_abortasync_abortasync_abort(reason="not_ipv4_address")
172 
173  await self.async_set_unique_idasync_set_unique_id(macaddress)
174  host = discovery_info.host
175  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_HOST: host})
176 
177  self._async_abort_entries_match_async_abort_entries_match({CONF_HOST: host})
178 
179  if not await async_verify_supported_device(self.hass, host):
180  return self.async_abortasync_abortasync_abort(reason="not_doorbird_device")
181 
182  chop_ending = "._axis-video._tcp.local."
183  friendly_hostname = discovery_info.name.removesuffix(chop_ending)
184 
185  self.context["title_placeholders"] = {
186  CONF_NAME: friendly_hostname,
187  CONF_HOST: host,
188  }
189  self.discovery_schemadiscovery_schema = _schema_with_defaults(host=host, name=friendly_hostname)
190 
191  return await self.async_step_userasync_step_userasync_step_user()
192 
194  self, user_input: dict[str, Any]
195  ) -> tuple[dict[str, Any], dict[str, Any]]:
196  """Validate doorbird or error."""
197  errors = {}
198  info = {}
199  try:
200  info = await validate_input(self.hass, user_input)
201  except CannotConnect:
202  errors["base"] = "cannot_connect"
203  except InvalidAuth:
204  errors["base"] = "invalid_auth"
205  except Exception:
206  _LOGGER.exception("Unexpected exception")
207  errors["base"] = "unknown"
208  return info, errors
209 
210  @staticmethod
211  @callback
213  config_entry: ConfigEntry,
214  ) -> OptionsFlowHandler:
215  """Get the options flow for this handler."""
216  return OptionsFlowHandler()
217 
218 
220  """Handle a option flow for doorbird."""
221 
222  async def async_step_init(
223  self, user_input: dict[str, Any] | None = None
224  ) -> ConfigFlowResult:
225  """Handle options flow."""
226  if user_input is not None:
227  events = [event.strip() for event in user_input[CONF_EVENTS].split(",")]
228  return self.async_create_entryasync_create_entry(title="", data={CONF_EVENTS: events})
229 
230  current_events = self.config_entryconfig_entryconfig_entry.options.get(CONF_EVENTS, [])
231 
232  # We convert to a comma separated list for the UI
233  # since there really isn't anything better
234  options_schema = vol.Schema(
235  {vol.Optional(CONF_EVENTS, default=", ".join(current_events)): str}
236  )
237  return self.async_show_formasync_show_form(step_id="init", data_schema=options_schema)
238 
239 
241  """Error to indicate we cannot connect."""
242 
243 
244 class InvalidAuth(HomeAssistantError):
245  """Error to indicate there is invalid auth."""
OptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:214
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:115
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:145
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:162
tuple[dict[str, Any], dict[str, Any]] _async_validate_or_error(self, dict[str, Any] user_input)
Definition: config_flow.py:195
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:108
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:224
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_step_user(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
None _async_abort_entries_match(self, dict[str, Any]|None match_dict=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)
None config_entry(self, ConfigEntry value)
_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)
bool async_verify_supported_device(HomeAssistant hass, str host)
Definition: config_flow.py:81
vol.Schema _schema_with_defaults(str|None host=None, str|None name=None)
Definition: config_flow.py:50
dict[str, str] validate_input(HomeAssistant hass, dict[str, Any] data)
Definition: config_flow.py:60
str get_mac_address_from_door_station_info(dict[str, Any] door_station_info)
Definition: util.py:12
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)