mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
Add reauth support to doorbird (#121815)
This commit is contained in:
parent
726fcb485d
commit
a8321fac95
@ -17,7 +17,7 @@ from homeassistant.const import (
|
|||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
@ -52,28 +52,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: DoorBirdConfigEntry) ->
|
|||||||
|
|
||||||
device = DoorBird(device_ip, username, password, http_session=session)
|
device = DoorBird(device_ip, username, password, http_session=session)
|
||||||
try:
|
try:
|
||||||
status = await device.ready()
|
|
||||||
info = await device.info()
|
info = await device.info()
|
||||||
except ClientResponseError as err:
|
except ClientResponseError as err:
|
||||||
if err.status == HTTPStatus.UNAUTHORIZED:
|
if err.status == HTTPStatus.UNAUTHORIZED:
|
||||||
_LOGGER.error(
|
raise ConfigEntryAuthFailed from err
|
||||||
"Authorization rejected by DoorBird for %s@%s", username, device_ip
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
raise ConfigEntryNotReady from err
|
raise ConfigEntryNotReady from err
|
||||||
except OSError as oserr:
|
except OSError as oserr:
|
||||||
_LOGGER.error("Failed to setup doorbird at %s: %s", device_ip, oserr)
|
|
||||||
raise ConfigEntryNotReady from oserr
|
raise ConfigEntryNotReady from oserr
|
||||||
|
|
||||||
if not status[0]:
|
|
||||||
_LOGGER.error(
|
|
||||||
"Could not connect to DoorBird as %s@%s: Error %s",
|
|
||||||
username,
|
|
||||||
device_ip,
|
|
||||||
str(status[1]),
|
|
||||||
)
|
|
||||||
raise ConfigEntryNotReady
|
|
||||||
|
|
||||||
token: str = door_station_config.get(CONF_TOKEN, config_entry_id)
|
token: str = door_station_config.get(CONF_TOKEN, config_entry_id)
|
||||||
custom_url: str | None = door_station_config.get(CONF_CUSTOM_URL)
|
custom_url: str | None = door_station_config.get(CONF_CUSTOM_URL)
|
||||||
name: str | None = door_station_config.get(CONF_NAME)
|
name: str | None = door_station_config.get(CONF_NAME)
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@ -21,6 +22,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNA
|
|||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.typing import VolDictType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_EVENTS,
|
CONF_EVENTS,
|
||||||
@ -36,14 +38,20 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
DEFAULT_OPTIONS = {CONF_EVENTS: [DEFAULT_DOORBELL_EVENT, DEFAULT_MOTION_EVENT]}
|
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(
|
def _schema_with_defaults(
|
||||||
host: str | None = None, name: str | None = None
|
host: str | None = None, name: str | None = None
|
||||||
) -> vol.Schema:
|
) -> vol.Schema:
|
||||||
return vol.Schema(
|
return vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_HOST, default=host): str,
|
vol.Required(CONF_HOST, default=host): str,
|
||||||
vol.Required(CONF_USERNAME): str,
|
**AUTH_VOL_DICT,
|
||||||
vol.Required(CONF_PASSWORD): str,
|
|
||||||
vol.Optional(CONF_NAME, default=name): str,
|
vol.Optional(CONF_NAME, default=name): str,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -56,7 +64,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
|||||||
data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD], http_session=session
|
data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD], http_session=session
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
status = await device.ready()
|
|
||||||
info = await device.info()
|
info = await device.info()
|
||||||
except ClientResponseError as err:
|
except ClientResponseError as err:
|
||||||
if err.status == HTTPStatus.UNAUTHORIZED:
|
if err.status == HTTPStatus.UNAUTHORIZED:
|
||||||
@ -65,9 +72,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
|||||||
except OSError as err:
|
except OSError as err:
|
||||||
raise CannotConnect from err
|
raise CannotConnect from err
|
||||||
|
|
||||||
if not status[0]:
|
|
||||||
raise CannotConnect
|
|
||||||
|
|
||||||
mac_addr = get_mac_address_from_door_station_info(info)
|
mac_addr = get_mac_address_from_door_station_info(info)
|
||||||
|
|
||||||
# Return info that you want to store in the config entry.
|
# Return info that you want to store in the config entry.
|
||||||
@ -96,6 +100,47 @@ class DoorBirdConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize the DoorBird config flow."""
|
"""Initialize the DoorBird config flow."""
|
||||||
self.discovery_schema: vol.Schema | None = None
|
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(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
@ -23,12 +23,20 @@
|
|||||||
"data_description": {
|
"data_description": {
|
||||||
"host": "The hostname or IP address of your DoorBird device."
|
"host": "The hostname or IP address of your DoorBird device."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"reauth_confirm": {
|
||||||
|
"description": "Re-authenticate DoorBird device {name} at {host}",
|
||||||
|
"data": {
|
||||||
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
"link_local_address": "Link local addresses are not supported",
|
"link_local_address": "Link local addresses are not supported",
|
||||||
"not_doorbird_device": "This device is not a DoorBird"
|
"not_doorbird_device": "This device is not a DoorBird",
|
||||||
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||||
},
|
},
|
||||||
"flow_title": "{name} ({host})",
|
"flow_title": "{name} ({host})",
|
||||||
"error": {
|
"error": {
|
||||||
|
@ -28,9 +28,8 @@ VALID_CONFIG = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _get_mock_doorbirdapi_return_values(ready=None, info=None):
|
def _get_mock_doorbirdapi_return_values(info=None):
|
||||||
doorbirdapi_mock = MagicMock()
|
doorbirdapi_mock = MagicMock()
|
||||||
type(doorbirdapi_mock).ready = AsyncMock(return_value=ready)
|
|
||||||
type(doorbirdapi_mock).info = AsyncMock(return_value=info)
|
type(doorbirdapi_mock).info = AsyncMock(return_value=info)
|
||||||
type(doorbirdapi_mock).doorbell_state = AsyncMock(
|
type(doorbirdapi_mock).doorbell_state = AsyncMock(
|
||||||
side_effect=aiohttp.ClientResponseError(
|
side_effect=aiohttp.ClientResponseError(
|
||||||
@ -40,9 +39,8 @@ def _get_mock_doorbirdapi_return_values(ready=None, info=None):
|
|||||||
return doorbirdapi_mock
|
return doorbirdapi_mock
|
||||||
|
|
||||||
|
|
||||||
def _get_mock_doorbirdapi_side_effects(ready=None, info=None):
|
def _get_mock_doorbirdapi_side_effects(info=None):
|
||||||
doorbirdapi_mock = MagicMock()
|
doorbirdapi_mock = MagicMock()
|
||||||
type(doorbirdapi_mock).ready = AsyncMock(side_effect=ready)
|
|
||||||
type(doorbirdapi_mock).info = AsyncMock(side_effect=info)
|
type(doorbirdapi_mock).info = AsyncMock(side_effect=info)
|
||||||
|
|
||||||
return doorbirdapi_mock
|
return doorbirdapi_mock
|
||||||
@ -57,9 +55,7 @@ async def test_user_form(hass: HomeAssistant) -> None:
|
|||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["errors"] == {}
|
assert result["errors"] == {}
|
||||||
|
|
||||||
doorbirdapi = _get_mock_doorbirdapi_return_values(
|
doorbirdapi = _get_mock_doorbirdapi_return_values(info={"WIFI_MAC_ADDR": "macaddr"})
|
||||||
ready=[True], info={"WIFI_MAC_ADDR": "macaddr"}
|
|
||||||
)
|
|
||||||
with (
|
with (
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.doorbird.config_flow.DoorBird",
|
"homeassistant.components.doorbird.config_flow.DoorBird",
|
||||||
@ -184,9 +180,7 @@ async def test_form_zeroconf_non_ipv4_ignored(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
async def test_form_zeroconf_correct_oui(hass: HomeAssistant) -> None:
|
async def test_form_zeroconf_correct_oui(hass: HomeAssistant) -> None:
|
||||||
"""Test we can setup from zeroconf with the correct OUI source."""
|
"""Test we can setup from zeroconf with the correct OUI source."""
|
||||||
doorbirdapi = _get_mock_doorbirdapi_return_values(
|
doorbirdapi = _get_mock_doorbirdapi_return_values(info={"WIFI_MAC_ADDR": "macaddr"})
|
||||||
ready=[True], info={"WIFI_MAC_ADDR": "macaddr"}
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.doorbird.config_flow.DoorBird",
|
"homeassistant.components.doorbird.config_flow.DoorBird",
|
||||||
@ -253,9 +247,7 @@ async def test_form_zeroconf_correct_oui_wrong_device(
|
|||||||
hass: HomeAssistant, doorbell_state_side_effect
|
hass: HomeAssistant, doorbell_state_side_effect
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test we can setup from zeroconf with the correct OUI source but not a doorstation."""
|
"""Test we can setup from zeroconf with the correct OUI source but not a doorstation."""
|
||||||
doorbirdapi = _get_mock_doorbirdapi_return_values(
|
doorbirdapi = _get_mock_doorbirdapi_return_values(info={"WIFI_MAC_ADDR": "macaddr"})
|
||||||
ready=[True], info={"WIFI_MAC_ADDR": "macaddr"}
|
|
||||||
)
|
|
||||||
type(doorbirdapi).doorbell_state = AsyncMock(side_effect=doorbell_state_side_effect)
|
type(doorbirdapi).doorbell_state = AsyncMock(side_effect=doorbell_state_side_effect)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
@ -286,7 +278,7 @@ async def test_form_user_cannot_connect(hass: HomeAssistant) -> None:
|
|||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
|
|
||||||
doorbirdapi = _get_mock_doorbirdapi_side_effects(ready=OSError)
|
doorbirdapi = _get_mock_doorbirdapi_side_effects(info=OSError)
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.doorbird.config_flow.DoorBird",
|
"homeassistant.components.doorbird.config_flow.DoorBird",
|
||||||
return_value=doorbirdapi,
|
return_value=doorbirdapi,
|
||||||
@ -309,7 +301,7 @@ async def test_form_user_invalid_auth(hass: HomeAssistant) -> None:
|
|||||||
mock_error = aiohttp.ClientResponseError(
|
mock_error = aiohttp.ClientResponseError(
|
||||||
request_info=Mock(), history=Mock(), status=401
|
request_info=Mock(), history=Mock(), status=401
|
||||||
)
|
)
|
||||||
doorbirdapi = _get_mock_doorbirdapi_side_effects(ready=mock_error)
|
doorbirdapi = _get_mock_doorbirdapi_side_effects(info=mock_error)
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.doorbird.config_flow.DoorBird",
|
"homeassistant.components.doorbird.config_flow.DoorBird",
|
||||||
return_value=doorbirdapi,
|
return_value=doorbirdapi,
|
||||||
@ -348,3 +340,69 @@ async def test_options_flow(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
assert config_entry.options == {CONF_EVENTS: ["eventa", "eventc", "eventq"]}
|
assert config_entry.options == {CONF_EVENTS: ["eventa", "eventc", "eventq"]}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reauth(hass: HomeAssistant) -> None:
|
||||||
|
"""Test reauth flow."""
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_HOST: "1.1.1.1",
|
||||||
|
CONF_NAME: "DoorBird",
|
||||||
|
CONF_USERNAME: "test-username",
|
||||||
|
CONF_PASSWORD: "test-password",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
config_entry.async_start_reauth(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN)
|
||||||
|
assert len(flows) == 1
|
||||||
|
flow = flows[0]
|
||||||
|
|
||||||
|
mock_error = aiohttp.ClientResponseError(
|
||||||
|
request_info=Mock(), history=Mock(), status=401
|
||||||
|
)
|
||||||
|
doorbirdapi = _get_mock_doorbirdapi_side_effects(info=mock_error)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.doorbird.config_flow.DoorBird",
|
||||||
|
return_value=doorbirdapi,
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
flow["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_USERNAME: "test-username",
|
||||||
|
CONF_PASSWORD: "test-password",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] is FlowResultType.FORM
|
||||||
|
assert result2["errors"] == {"base": "invalid_auth"}
|
||||||
|
|
||||||
|
doorbirdapi = _get_mock_doorbirdapi_return_values(info={"WIFI_MAC_ADDR": "macaddr"})
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.doorbird.config_flow.DoorBird",
|
||||||
|
return_value=doorbirdapi,
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.doorbird.async_setup", return_value=True
|
||||||
|
) as mock_setup,
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.doorbird.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
) as mock_setup_entry,
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
flow["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_USERNAME: "test-username",
|
||||||
|
CONF_PASSWORD: "test-password",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] is FlowResultType.ABORT
|
||||||
|
assert result2["reason"] == "reauth_successful"
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
Loading…
x
Reference in New Issue
Block a user