"""Config flow for DoorBird integration.""" from __future__ import annotations from collections.abc import Mapping from http import HTTPStatus import logging from typing import Any from aiohttp import ClientResponseError from doorbirdpy import DoorBird import voluptuous as vol from homeassistant.components import zeroconf from homeassistant.config_entries import ( ConfigEntry, ConfigFlow, ConfigFlowResult, OptionsFlow, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import VolDictType from .const import ( CONF_EVENTS, DEFAULT_DOORBELL_EVENT, DEFAULT_MOTION_EVENT, DOMAIN, DOORBIRD_OUI, ) from .util import get_mac_address_from_door_station_info _LOGGER = logging.getLogger(__name__) DEFAULT_OPTIONS = {CONF_EVENTS: [DEFAULT_DOORBELL_EVENT, DEFAULT_MOTION_EVENT]} AUTH_VOL_DICT: VolDictType = { vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, } AUTH_SCHEMA = vol.Schema(AUTH_VOL_DICT) def _schema_with_defaults( host: str | None = None, name: str | None = None ) -> vol.Schema: return vol.Schema( { vol.Required(CONF_HOST, default=host): str, **AUTH_VOL_DICT, vol.Optional(CONF_NAME, default=name): str, } ) async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: """Validate the user input allows us to connect.""" session = async_get_clientsession(hass) device = DoorBird( data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD], http_session=session ) try: info = await device.info() except ClientResponseError as err: if err.status == HTTPStatus.UNAUTHORIZED: raise InvalidAuth from err raise CannotConnect from err except OSError as err: raise CannotConnect from err mac_addr = get_mac_address_from_door_station_info(info) # Return info that you want to store in the config entry. return {"title": data[CONF_HOST], "mac_addr": mac_addr} async def async_verify_supported_device(hass: HomeAssistant, host: str) -> bool: """Verify the doorbell state endpoint returns a 401.""" session = async_get_clientsession(hass) device = DoorBird(host, "", "", http_session=session) try: await device.doorbell_state() except ClientResponseError as err: if err.status == HTTPStatus.UNAUTHORIZED: return True except OSError: return False return False class DoorBirdConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for DoorBird.""" VERSION = 1 def __init__(self) -> None: """Initialize the DoorBird config flow.""" self.discovery_schema: vol.Schema | None = None self.reauth_entry: ConfigEntry | None = None async def async_step_reauth( self, entry_data: Mapping[str, Any] ) -> ConfigFlowResult: """Handle reauth.""" entry_id = self.context["entry_id"] self.reauth_entry = self.hass.config_entries.async_get_entry(entry_id) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle reauth input.""" errors: dict[str, str] = {} existing_entry = self.reauth_entry assert existing_entry existing_data = existing_entry.data placeholders: dict[str, str] = { CONF_NAME: existing_data[CONF_NAME], CONF_HOST: existing_data[CONF_HOST], } self.context["title_placeholders"] = placeholders if user_input is not None: new_config = { **existing_data, CONF_USERNAME: user_input[CONF_USERNAME], CONF_PASSWORD: user_input[CONF_PASSWORD], } _, errors = await self._async_validate_or_error(new_config) if not errors: return self.async_update_reload_and_abort( existing_entry, data=new_config ) return self.async_show_form( description_placeholders=placeholders, step_id="reauth_confirm", data_schema=AUTH_SCHEMA, errors=errors, ) async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: info, errors = await self._async_validate_or_error(user_input) if not errors: await self.async_set_unique_id(info["mac_addr"]) self._abort_if_unique_id_configured() return self.async_create_entry( title=info["title"], data=user_input, options=DEFAULT_OPTIONS ) data = self.discovery_schema or _schema_with_defaults() return self.async_show_form(step_id="user", data_schema=data, errors=errors) async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> ConfigFlowResult: """Prepare configuration for a discovered doorbird device.""" macaddress = discovery_info.properties["macaddress"] if macaddress[:6] != DOORBIRD_OUI: return self.async_abort(reason="not_doorbird_device") if discovery_info.ip_address.is_link_local: return self.async_abort(reason="link_local_address") if discovery_info.ip_address.version != 4: return self.async_abort(reason="not_ipv4_address") await self.async_set_unique_id(macaddress) host = discovery_info.host self._abort_if_unique_id_configured(updates={CONF_HOST: host}) self._async_abort_entries_match({CONF_HOST: host}) if not await async_verify_supported_device(self.hass, host): return self.async_abort(reason="not_doorbird_device") chop_ending = "._axis-video._tcp.local." friendly_hostname = discovery_info.name.removesuffix(chop_ending) self.context["title_placeholders"] = { CONF_NAME: friendly_hostname, CONF_HOST: host, } self.discovery_schema = _schema_with_defaults(host=host, name=friendly_hostname) return await self.async_step_user() async def _async_validate_or_error( self, user_input: dict[str, Any] ) -> tuple[dict[str, Any], dict[str, Any]]: """Validate doorbird or error.""" errors = {} info = {} try: info = await validate_input(self.hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" except Exception: _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" return info, errors @staticmethod @callback def async_get_options_flow( config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) class OptionsFlowHandler(OptionsFlow): """Handle a option flow for doorbird.""" def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle options flow.""" if user_input is not None: events = [event.strip() for event in user_input[CONF_EVENTS].split(",")] return self.async_create_entry(title="", data={CONF_EVENTS: events}) current_events = self.config_entry.options.get(CONF_EVENTS, []) # We convert to a comma separated list for the UI # since there really isn't anything better options_schema = vol.Schema( {vol.Optional(CONF_EVENTS, default=", ".join(current_events)): str} ) return self.async_show_form(step_id="init", data_schema=options_schema) class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth."""