Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions homeassistant/components/doorbird/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
"""Support for DoorBird devices."""
import asyncio
import logging
import urllib
from urllib.error import HTTPError

from aiohttp import web
from doorbirdpy import DoorBird
import requests
import voluptuous as vol

from homeassistant.components.http import HomeAssistantView
Expand Down Expand Up @@ -130,8 +129,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
device = DoorBird(device_ip, username, password)
try:
status, info = await hass.async_add_executor_job(_init_doorbird_device, device)
except urllib.error.HTTPError as err:
if err.code == HTTP_UNAUTHORIZED:
except requests.exceptions.HTTPError as err:
if err.response.status_code == HTTP_UNAUTHORIZED:
_LOGGER.error(
"Authorization rejected by DoorBird for %s@%s", username, device_ip
)
Expand Down Expand Up @@ -202,7 +201,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
async def _async_register_events(hass, doorstation):
try:
await hass.async_add_executor_job(doorstation.register_events, hass)
except HTTPError:
except requests.exceptions.HTTPError:
hass.components.persistent_notification.async_create(
"Doorbird configuration failed. Please verify that API "
"Operator permission is enabled for the Doorbird user. "
Expand Down
47 changes: 30 additions & 17 deletions homeassistant/components/doorbird/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""Config flow for DoorBird integration."""
from ipaddress import ip_address
import logging
import urllib

from doorbirdpy import DoorBird
import requests
import voluptuous as vol

from homeassistant import config_entries, core, exceptions
Expand Down Expand Up @@ -34,17 +34,18 @@ def _schema_with_defaults(host=None, name=None):
)


async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect.
def _check_device(device):
"""Verify we can connect to the device and return the status."""
return device.ready(), device.info()


Data has the keys from DATA_SCHEMA with values provided by the user.
"""
async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect."""
device = DoorBird(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD])
try:
status = await hass.async_add_executor_job(device.ready)
info = await hass.async_add_executor_job(device.info)
except urllib.error.HTTPError as err:
if err.code == HTTP_UNAUTHORIZED:
status, info = await hass.async_add_executor_job(_check_device, device)
except requests.exceptions.HTTPError as err:
if err.response.status_code == HTTP_UNAUTHORIZED:
raise InvalidAuth from err
raise CannotConnect from err
except OSError as err:
Expand All @@ -59,6 +60,19 @@ async def validate_input(hass: core.HomeAssistant, data):
return {"title": data[CONF_HOST], "mac_addr": mac_addr}


async def async_verify_supported_device(hass, host):
"""Verify the doorbell state endpoint returns a 401."""
device = DoorBird(host, "", "")
try:
await hass.async_add_executor_job(device.doorbell_state)
except requests.exceptions.HTTPError as err:
if err.response.status_code == HTTP_UNAUTHORIZED:
return True
except OSError:
return False
return False


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for DoorBird."""

Expand All @@ -85,17 +99,18 @@ async def async_step_user(self, user_input=None):
async def async_step_zeroconf(self, discovery_info):
"""Prepare configuration for a discovered doorbird device."""
macaddress = discovery_info["properties"]["macaddress"]
host = discovery_info[CONF_HOST]

if macaddress[:6] != DOORBIRD_OUI:
return self.async_abort(reason="not_doorbird_device")
if is_link_local(ip_address(discovery_info[CONF_HOST])):
if is_link_local(ip_address(host)):
return self.async_abort(reason="link_local_address")
if not await async_verify_supported_device(self.hass, host):
return self.async_abort(reason="not_doorbird_device")

await self.async_set_unique_id(macaddress)

self._abort_if_unique_id_configured(
updates={CONF_HOST: discovery_info[CONF_HOST]}
)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})

chop_ending = "._axis-video._tcp.local."
friendly_hostname = discovery_info["name"]
Expand All @@ -104,11 +119,9 @@ async def async_step_zeroconf(self, discovery_info):

self.context["title_placeholders"] = {
CONF_NAME: friendly_hostname,
CONF_HOST: discovery_info[CONF_HOST],
CONF_HOST: host,
}
self.discovery_schema = _schema_with_defaults(
host=discovery_info[CONF_HOST], name=friendly_hostname
)
self.discovery_schema = _schema_with_defaults(host=host, name=friendly_hostname)

return await self.async_step_user()

Expand Down
113 changes: 81 additions & 32 deletions tests/components/doorbird/test_config_flow.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Test the DoorBird config flow."""
from unittest.mock import MagicMock, patch
import urllib
from unittest.mock import MagicMock, Mock, patch

import pytest
import requests

from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.doorbird import CONF_CUSTOM_URL, CONF_TOKEN
Expand All @@ -21,7 +23,9 @@ def _get_mock_doorbirdapi_return_values(ready=None, info=None):
doorbirdapi_mock = MagicMock()
type(doorbirdapi_mock).ready = MagicMock(return_value=ready)
type(doorbirdapi_mock).info = MagicMock(return_value=info)

type(doorbirdapi_mock).doorbell_state = MagicMock(
side_effect=requests.exceptions.HTTPError(response=Mock(status_code=401))
)
return doorbirdapi_mock


Expand Down Expand Up @@ -137,17 +141,25 @@ async def test_form_import_with_zeroconf_already_discovered(hass):

await setup.async_setup_component(hass, "persistent_notification", {})

doorbirdapi = _get_mock_doorbirdapi_return_values(
ready=[True], info={"WIFI_MAC_ADDR": "1CCAE3DOORBIRD"}
)
# Running the zeroconf init will make the unique id
# in progress
zero_conf = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data={
"properties": {"macaddress": "1CCAE3DOORBIRD"},
"name": "Doorstation - abc123._axis-video._tcp.local.",
"host": "192.168.1.5",
},
)
with patch(
"homeassistant.components.doorbird.config_flow.DoorBird",
return_value=doorbirdapi,
):
zero_conf = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data={
"properties": {"macaddress": "1CCAE3DOORBIRD"},
"name": "Doorstation - abc123._axis-video._tcp.local.",
"host": "192.168.1.5",
},
)
await hass.async_block_till_done()
assert zero_conf["type"] == data_entry_flow.RESULT_TYPE_FORM
assert zero_conf["step_id"] == "user"
assert zero_conf["errors"] == {}
Expand All @@ -159,9 +171,6 @@ async def test_form_import_with_zeroconf_already_discovered(hass):
CONF_CUSTOM_URL
] = "http://legacy.custom.url/should/only/come/in/from/yaml"

doorbirdapi = _get_mock_doorbirdapi_return_values(
ready=[True], info={"WIFI_MAC_ADDR": "1CCAE3DOORBIRD"}
)
with patch(
"homeassistant.components.doorbird.config_flow.DoorBird",
return_value=doorbirdapi,
Expand Down Expand Up @@ -244,24 +253,29 @@ async def test_form_zeroconf_correct_oui(hass):
await hass.async_add_executor_job(
init_recorder_component, hass
) # force in memory db

doorbirdapi = _get_mock_doorbirdapi_return_values(
ready=[True], info={"WIFI_MAC_ADDR": "macaddr"}
)
await setup.async_setup_component(hass, "persistent_notification", {})

result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data={
"properties": {"macaddress": "1CCAE3DOORBIRD"},
"name": "Doorstation - abc123._axis-video._tcp.local.",
"host": "192.168.1.5",
},
)
with patch(
"homeassistant.components.doorbird.config_flow.DoorBird",
return_value=doorbirdapi,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data={
"properties": {"macaddress": "1CCAE3DOORBIRD"},
"name": "Doorstation - abc123._axis-video._tcp.local.",
"host": "192.168.1.5",
},
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
doorbirdapi = _get_mock_doorbirdapi_return_values(
ready=[True], info={"WIFI_MAC_ADDR": "macaddr"}
)

with patch(
"homeassistant.components.doorbird.config_flow.DoorBird",
return_value=doorbirdapi,
Expand All @@ -288,6 +302,43 @@ async def test_form_zeroconf_correct_oui(hass):
assert len(mock_setup_entry.mock_calls) == 1


@pytest.mark.parametrize(
"doorbell_state_side_effect",
[
requests.exceptions.HTTPError(response=Mock(status_code=404)),
OSError,
None,
],
)
async def test_form_zeroconf_correct_oui_wrong_device(hass, doorbell_state_side_effect):
"""Test we can setup from zeroconf with the correct OUI source but not a doorstation."""
await hass.async_add_executor_job(
init_recorder_component, hass
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know why we need to set up the recorder? I don't remember the reason.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't remember the exact reason but I do remember the tests would fail the CI without it

Let's do an experiment and see if we can take it out #48781

) # force in memory db
doorbirdapi = _get_mock_doorbirdapi_return_values(
ready=[True], info={"WIFI_MAC_ADDR": "macaddr"}
)
type(doorbirdapi).doorbell_state = MagicMock(side_effect=doorbell_state_side_effect)
await setup.async_setup_component(hass, "persistent_notification", {})

with patch(
"homeassistant.components.doorbird.config_flow.DoorBird",
return_value=doorbirdapi,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data={
"properties": {"macaddress": "1CCAE3DOORBIRD"},
"name": "Doorstation - abc123._axis-video._tcp.local.",
"host": "192.168.1.5",
},
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "not_doorbird_device"


async def test_form_user_cannot_connect(hass):
"""Test we handle cannot connect error."""
await hass.async_add_executor_job(
Expand Down Expand Up @@ -322,10 +373,8 @@ async def test_form_user_invalid_auth(hass):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)

mock_urllib_error = urllib.error.HTTPError(
"http://xyz.tld", 401, "login failed", {}, None
)
doorbirdapi = _get_mock_doorbirdapi_side_effects(ready=mock_urllib_error)
mock_error = requests.exceptions.HTTPError(response=Mock(status_code=401))
doorbirdapi = _get_mock_doorbirdapi_side_effects(ready=mock_error)
with patch(
"homeassistant.components.doorbird.config_flow.DoorBird",
return_value=doorbirdapi,
Expand Down