mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Convert doorbird to use asyncio (#121569)
This commit is contained in:
parent
020961d2d8
commit
0e0a339517
@ -4,10 +4,9 @@ from __future__ import annotations
|
||||
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from doorbirdpy import DoorBird
|
||||
import requests
|
||||
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.const import (
|
||||
@ -19,6 +18,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
@ -48,12 +48,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: DoorBirdConfigEntry) ->
|
||||
device_ip = door_station_config[CONF_HOST]
|
||||
username = door_station_config[CONF_USERNAME]
|
||||
password = door_station_config[CONF_PASSWORD]
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
device = DoorBird(device_ip, username, password)
|
||||
device = DoorBird(device_ip, username, password, http_session=session)
|
||||
try:
|
||||
status, info = await hass.async_add_executor_job(_init_door_bird_device, device)
|
||||
except requests.exceptions.HTTPError as err:
|
||||
if err.response.status_code == HTTPStatus.UNAUTHORIZED:
|
||||
status = await device.ready()
|
||||
info = await device.info()
|
||||
except ClientResponseError as err:
|
||||
if err.status == HTTPStatus.UNAUTHORIZED:
|
||||
_LOGGER.error(
|
||||
"Authorization rejected by DoorBird for %s@%s", username, device_ip
|
||||
)
|
||||
@ -91,11 +93,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: DoorBirdConfigEntry) ->
|
||||
return True
|
||||
|
||||
|
||||
def _init_door_bird_device(device: DoorBird) -> tuple[tuple[bool, int], dict[str, Any]]:
|
||||
"""Verify we can connect to the device and return the status."""
|
||||
return device.ready(), device.info()
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: DoorBirdConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
@ -106,8 +103,8 @@ async def _async_register_events(
|
||||
) -> bool:
|
||||
"""Register events on device."""
|
||||
try:
|
||||
await hass.async_add_executor_job(door_station.register_events, hass)
|
||||
except requests.exceptions.HTTPError:
|
||||
await door_station.async_register_events(hass)
|
||||
except ClientResponseError:
|
||||
persistent_notification.async_create(
|
||||
hass,
|
||||
(
|
||||
|
@ -1,7 +1,8 @@
|
||||
"""Support for powering relays in a DoorBird video doorbell."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from doorbirdpy import DoorBird
|
||||
|
||||
@ -19,7 +20,7 @@ IR_RELAY = "__ir_light__"
|
||||
class DoorbirdButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Class to describe a Doorbird Button entity."""
|
||||
|
||||
press_action: Callable[[DoorBird, str], None]
|
||||
press_action: Callable[[DoorBird, str], Coroutine[Any, Any, bool]]
|
||||
|
||||
|
||||
RELAY_ENTITY_DESCRIPTION = DoorbirdButtonEntityDescription(
|
||||
@ -73,6 +74,8 @@ class DoorBirdButton(DoorBirdEntity, ButtonEntity):
|
||||
self._attr_name = f"Relay {self._relay}"
|
||||
self._attr_unique_id = f"{self._mac_addr}_{self._relay}"
|
||||
|
||||
def press(self) -> None:
|
||||
async def async_press(self) -> None:
|
||||
"""Power the relay."""
|
||||
self.entity_description.press_action(self._door_station.device, self._relay)
|
||||
await self.entity_description.press_action(
|
||||
self._door_station.device, self._relay
|
||||
)
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
@ -10,7 +9,6 @@ import aiohttp
|
||||
|
||||
from homeassistant.components.camera import Camera, CameraEntityFeature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
@ -95,11 +93,9 @@ class DoorBirdCamera(DoorBirdEntity, Camera):
|
||||
return self._last_image
|
||||
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
async with asyncio.timeout(_TIMEOUT):
|
||||
response = await websession.get(self._url)
|
||||
|
||||
self._last_image = await response.read()
|
||||
self._last_image = await self._door_station.device.get_image(
|
||||
self._url, timeout=_TIMEOUT
|
||||
)
|
||||
except TimeoutError:
|
||||
_LOGGER.error("DoorBird %s: Camera image timed out", self.name)
|
||||
return self._last_image
|
||||
|
@ -6,8 +6,8 @@ from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from doorbirdpy import DoorBird
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
@ -20,6 +20,7 @@ from homeassistant.config_entries import (
|
||||
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 .const import CONF_EVENTS, DOMAIN, DOORBIRD_OUI
|
||||
from .util import get_mac_address_from_door_station_info
|
||||
@ -40,18 +41,17 @@ def _schema_with_defaults(
|
||||
)
|
||||
|
||||
|
||||
def _check_device(device: DoorBird) -> tuple[tuple[bool, int], dict[str, Any]]:
|
||||
"""Verify we can connect to the device and return the status."""
|
||||
return device.ready(), device.info()
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]:
|
||||
"""Validate the user input allows us to connect."""
|
||||
device = DoorBird(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD])
|
||||
session = async_get_clientsession(hass)
|
||||
device = DoorBird(
|
||||
data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD], http_session=session
|
||||
)
|
||||
try:
|
||||
status, info = await hass.async_add_executor_job(_check_device, device)
|
||||
except requests.exceptions.HTTPError as err:
|
||||
if err.response.status_code == HTTPStatus.UNAUTHORIZED:
|
||||
status = await device.ready()
|
||||
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:
|
||||
@ -68,11 +68,12 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
|
||||
async def async_verify_supported_device(hass: HomeAssistant, host: str) -> bool:
|
||||
"""Verify the doorbell state endpoint returns a 401."""
|
||||
device = DoorBird(host, "", "")
|
||||
session = async_get_clientsession(hass)
|
||||
device = DoorBird(host, "", "", http_session=session)
|
||||
try:
|
||||
await hass.async_add_executor_job(device.doorbell_state)
|
||||
except requests.exceptions.HTTPError as err:
|
||||
if err.response.status_code == HTTPStatus.UNAUTHORIZED:
|
||||
await device.doorbell_state()
|
||||
except ClientResponseError as err:
|
||||
if err.status == HTTPStatus.UNAUTHORIZED:
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
@ -75,7 +75,7 @@ class ConfiguredDoorBird:
|
||||
"""Get token for device."""
|
||||
return self._token
|
||||
|
||||
def register_events(self, hass: HomeAssistant) -> None:
|
||||
async def async_register_events(self, hass: HomeAssistant) -> None:
|
||||
"""Register events on device."""
|
||||
# Override url if another is specified in the configuration
|
||||
if custom_url := self.custom_url:
|
||||
@ -88,14 +88,14 @@ class ConfiguredDoorBird:
|
||||
# User may not have permission to get the favorites
|
||||
return
|
||||
|
||||
favorites = self.device.favorites()
|
||||
favorites = await self.device.favorites()
|
||||
for event in self.door_station_events:
|
||||
if self._register_event(hass_url, event, favs=favorites):
|
||||
if await self._async_register_event(hass_url, event, favs=favorites):
|
||||
_LOGGER.info(
|
||||
"Successfully registered URL for %s on %s", event, self.name
|
||||
)
|
||||
|
||||
schedule: list[DoorBirdScheduleEntry] = self.device.schedule()
|
||||
schedule: list[DoorBirdScheduleEntry] = await self.device.schedule()
|
||||
http_fav: dict[str, dict[str, Any]] = favorites.get("http") or {}
|
||||
favorite_input_type: dict[str, str] = {
|
||||
output.param: entry.input
|
||||
@ -122,18 +122,18 @@ class ConfiguredDoorBird:
|
||||
def _get_event_name(self, event: str) -> str:
|
||||
return f"{self.slug}_{event}"
|
||||
|
||||
def _register_event(
|
||||
async def _async_register_event(
|
||||
self, hass_url: str, event: str, favs: dict[str, Any] | None = None
|
||||
) -> bool:
|
||||
"""Add a schedule entry in the device for a sensor."""
|
||||
url = f"{hass_url}{API_URL}/{event}?token={self._token}"
|
||||
|
||||
# Register HA URL as webhook if not already, then get the ID
|
||||
if self.webhook_is_registered(url, favs=favs):
|
||||
if await self.async_webhook_is_registered(url, favs=favs):
|
||||
return True
|
||||
|
||||
self.device.change_favorite("http", f"Home Assistant ({event})", url)
|
||||
if not self.webhook_is_registered(url):
|
||||
await self.device.change_favorite("http", f"Home Assistant ({event})", url)
|
||||
if not await self.async_webhook_is_registered(url):
|
||||
_LOGGER.warning(
|
||||
'Unable to set favorite URL "%s". Event "%s" will not fire',
|
||||
url,
|
||||
@ -142,20 +142,20 @@ class ConfiguredDoorBird:
|
||||
return False
|
||||
return True
|
||||
|
||||
def webhook_is_registered(
|
||||
async def async_webhook_is_registered(
|
||||
self, url: str, favs: dict[str, Any] | None = None
|
||||
) -> bool:
|
||||
"""Return whether the given URL is registered as a device favorite."""
|
||||
return self.get_webhook_id(url, favs) is not None
|
||||
return await self.async_get_webhook_id(url, favs) is not None
|
||||
|
||||
def get_webhook_id(
|
||||
async def async_get_webhook_id(
|
||||
self, url: str, favs: dict[str, Any] | None = None
|
||||
) -> str | None:
|
||||
"""Return the device favorite ID for the given URL.
|
||||
|
||||
The favorite must exist or there will be problems.
|
||||
"""
|
||||
favs = favs if favs else self.device.favorites()
|
||||
favs = favs if favs else await self.device.favorites()
|
||||
http_fav: dict[str, dict[str, Any]] = favs.get("http") or {}
|
||||
for fav_id, data in http_fav.items():
|
||||
if data["value"] == url:
|
||||
@ -178,14 +178,8 @@ async def async_reset_device_favorites(
|
||||
hass: HomeAssistant, door_station: ConfiguredDoorBird
|
||||
) -> None:
|
||||
"""Handle clearing favorites on device."""
|
||||
await hass.async_add_executor_job(_reset_device_favorites, door_station)
|
||||
|
||||
|
||||
def _reset_device_favorites(door_station: ConfiguredDoorBird) -> None:
|
||||
"""Handle clearing favorites on device."""
|
||||
# Clear webhooks
|
||||
door_bird = door_station.device
|
||||
favorites: dict[str, list[str]] = door_bird.favorites()
|
||||
favorites: dict[str, dict[str, Any]] = await door_bird.favorites()
|
||||
for favorite_type, favorite_ids in favorites.items():
|
||||
for favorite_id in favorite_ids:
|
||||
door_bird.delete_favorite(favorite_type, favorite_id)
|
||||
await door_bird.delete_favorite(favorite_type, favorite_id)
|
||||
|
@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/doorbird",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["doorbirdpy"],
|
||||
"requirements": ["DoorBirdPy==2.1.0"],
|
||||
"requirements": ["DoorBirdPy==3.0.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_axis-video._tcp.local.",
|
||||
|
@ -16,7 +16,7 @@ Adax-local==0.1.5
|
||||
BlinkStick==1.2.0
|
||||
|
||||
# homeassistant.components.doorbird
|
||||
DoorBirdPy==2.1.0
|
||||
DoorBirdPy==3.0.0
|
||||
|
||||
# homeassistant.components.homekit
|
||||
HAP-python==4.9.1
|
||||
|
@ -13,7 +13,7 @@ AIOSomecomfort==0.0.25
|
||||
Adax-local==0.1.5
|
||||
|
||||
# homeassistant.components.doorbird
|
||||
DoorBirdPy==2.1.0
|
||||
DoorBirdPy==3.0.0
|
||||
|
||||
# homeassistant.components.homekit
|
||||
HAP-python==4.9.1
|
||||
|
@ -1,10 +1,10 @@
|
||||
"""Test the DoorBird config flow."""
|
||||
|
||||
from ipaddress import ip_address
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import aiohttp
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import zeroconf
|
||||
@ -25,18 +25,20 @@ VALID_CONFIG = {
|
||||
|
||||
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))
|
||||
type(doorbirdapi_mock).ready = AsyncMock(return_value=ready)
|
||||
type(doorbirdapi_mock).info = AsyncMock(return_value=info)
|
||||
type(doorbirdapi_mock).doorbell_state = AsyncMock(
|
||||
side_effect=aiohttp.ClientResponseError(
|
||||
request_info=Mock(), history=Mock(), status=401
|
||||
)
|
||||
)
|
||||
return doorbirdapi_mock
|
||||
|
||||
|
||||
def _get_mock_doorbirdapi_side_effects(ready=None, info=None):
|
||||
doorbirdapi_mock = MagicMock()
|
||||
type(doorbirdapi_mock).ready = MagicMock(side_effect=ready)
|
||||
type(doorbirdapi_mock).info = MagicMock(side_effect=info)
|
||||
type(doorbirdapi_mock).ready = AsyncMock(side_effect=ready)
|
||||
type(doorbirdapi_mock).info = AsyncMock(side_effect=info)
|
||||
|
||||
return doorbirdapi_mock
|
||||
|
||||
@ -234,7 +236,7 @@ async def test_form_zeroconf_correct_oui(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
"doorbell_state_side_effect",
|
||||
[
|
||||
requests.exceptions.HTTPError(response=Mock(status_code=404)),
|
||||
aiohttp.ClientResponseError(request_info=Mock(), history=Mock(), status=404),
|
||||
OSError,
|
||||
None,
|
||||
],
|
||||
@ -246,7 +248,7 @@ async def test_form_zeroconf_correct_oui_wrong_device(
|
||||
doorbirdapi = _get_mock_doorbirdapi_return_values(
|
||||
ready=[True], info={"WIFI_MAC_ADDR": "macaddr"}
|
||||
)
|
||||
type(doorbirdapi).doorbell_state = MagicMock(side_effect=doorbell_state_side_effect)
|
||||
type(doorbirdapi).doorbell_state = AsyncMock(side_effect=doorbell_state_side_effect)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.doorbird.config_flow.DoorBird",
|
||||
@ -296,7 +298,9 @@ async def test_form_user_invalid_auth(hass: HomeAssistant) -> None:
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
mock_error = requests.exceptions.HTTPError(response=Mock(status_code=401))
|
||||
mock_error = aiohttp.ClientResponseError(
|
||||
request_info=Mock(), history=Mock(), status=401
|
||||
)
|
||||
doorbirdapi = _get_mock_doorbirdapi_side_effects(ready=mock_error)
|
||||
with patch(
|
||||
"homeassistant.components.doorbird.config_flow.DoorBird",
|
||||
|
Loading…
x
Reference in New Issue
Block a user