Convert ring integration to the async ring-doorbell api (#124365)
* Bump ring-doorbell to 0.9.0 * Convert ring integration to async ring-doorbell api * Use mock auth fixture class to get token_updater * Fix typo in fixture name
This commit is contained in:
parent
7ae8f4c9d0
commit
e26d363b5e
21 changed files with 159 additions and 125 deletions
|
@ -3,7 +3,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
|
@ -13,6 +12,7 @@ from homeassistant.config_entries import ConfigEntry
|
|||
from homeassistant.const import APPLICATION_NAME, CONF_TOKEN, __version__
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
|
@ -35,17 +35,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
"""Set up a config entry."""
|
||||
|
||||
def token_updater(token: dict[str, Any]) -> None:
|
||||
"""Handle from sync context when token is updated."""
|
||||
hass.loop.call_soon_threadsafe(
|
||||
partial(
|
||||
hass.config_entries.async_update_entry,
|
||||
entry,
|
||||
data={**entry.data, CONF_TOKEN: token},
|
||||
)
|
||||
"""Handle from async context when token is updated."""
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
data={**entry.data, CONF_TOKEN: token},
|
||||
)
|
||||
|
||||
auth = Auth(
|
||||
f"{APPLICATION_NAME}/{__version__}", entry.data[CONF_TOKEN], token_updater
|
||||
f"{APPLICATION_NAME}/{__version__}",
|
||||
entry.data[CONF_TOKEN],
|
||||
token_updater,
|
||||
http_client_session=async_get_clientsession(hass),
|
||||
)
|
||||
ring = Ring(auth)
|
||||
|
||||
|
|
|
@ -53,6 +53,6 @@ class RingDoorButton(RingEntity[RingOther], ButtonEntity):
|
|||
self._attr_unique_id = f"{device.id}-{description.key}"
|
||||
|
||||
@exception_wrap
|
||||
def press(self) -> None:
|
||||
async def async_press(self) -> None:
|
||||
"""Open the door."""
|
||||
self._device.open_door()
|
||||
await self._device.async_open_door()
|
||||
|
|
|
@ -159,36 +159,36 @@ class RingCam(RingEntity[RingDoorBell], Camera):
|
|||
if self._last_video_id != self._last_event["id"]:
|
||||
self._image = None
|
||||
|
||||
self._video_url = await self.hass.async_add_executor_job(self._get_video)
|
||||
self._video_url = await self._async_get_video()
|
||||
|
||||
self._last_video_id = self._last_event["id"]
|
||||
self._expires_at = FORCE_REFRESH_INTERVAL + utcnow
|
||||
|
||||
@exception_wrap
|
||||
def _get_video(self) -> str | None:
|
||||
async def _async_get_video(self) -> str | None:
|
||||
if TYPE_CHECKING:
|
||||
# _last_event is set before calling update so will never be None
|
||||
assert self._last_event
|
||||
event_id = self._last_event.get("id")
|
||||
assert event_id and isinstance(event_id, int)
|
||||
return self._device.recording_url(event_id)
|
||||
return await self._device.async_recording_url(event_id)
|
||||
|
||||
@exception_wrap
|
||||
def _set_motion_detection_enabled(self, new_state: bool) -> None:
|
||||
async def _async_set_motion_detection_enabled(self, new_state: bool) -> None:
|
||||
if not self._device.has_capability(MOTION_DETECTION_CAPABILITY):
|
||||
_LOGGER.error(
|
||||
"Entity %s does not have motion detection capability", self.entity_id
|
||||
)
|
||||
return
|
||||
|
||||
self._device.motion_detection = new_state
|
||||
await self._device.async_set_motion_detection(new_state)
|
||||
self._attr_motion_detection_enabled = new_state
|
||||
self.schedule_update_ha_state(False)
|
||||
self.async_schedule_update_ha_state(False)
|
||||
|
||||
def enable_motion_detection(self) -> None:
|
||||
async def async_enable_motion_detection(self) -> None:
|
||||
"""Enable motion detection in the camera."""
|
||||
self._set_motion_detection_enabled(True)
|
||||
await self._async_set_motion_detection_enabled(True)
|
||||
|
||||
def disable_motion_detection(self) -> None:
|
||||
async def async_disable_motion_detection(self) -> None:
|
||||
"""Disable motion detection in camera."""
|
||||
self._set_motion_detection_enabled(False)
|
||||
await self._async_set_motion_detection_enabled(False)
|
||||
|
|
|
@ -34,8 +34,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str,
|
|||
auth = Auth(f"{APPLICATION_NAME}/{ha_version}")
|
||||
|
||||
try:
|
||||
token = await hass.async_add_executor_job(
|
||||
auth.fetch_token,
|
||||
token = await auth.async_fetch_token(
|
||||
data[CONF_USERNAME],
|
||||
data[CONF_PASSWORD],
|
||||
data.get(CONF_2FA),
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
"""Data coordinators for the ring integration."""
|
||||
|
||||
from asyncio import TaskGroup
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Coroutine
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from ring_doorbell import AuthenticationError, Ring, RingDevices, RingError, RingTimeout
|
||||
|
||||
|
@ -16,10 +17,13 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
|
||||
async def _call_api[*_Ts, _R](
|
||||
hass: HomeAssistant, target: Callable[[*_Ts], _R], *args: *_Ts, msg_suffix: str = ""
|
||||
hass: HomeAssistant,
|
||||
target: Callable[[*_Ts], Coroutine[Any, Any, _R]],
|
||||
*args: *_Ts,
|
||||
msg_suffix: str = "",
|
||||
) -> _R:
|
||||
try:
|
||||
return await hass.async_add_executor_job(target, *args)
|
||||
return await target(*args)
|
||||
except AuthenticationError as err:
|
||||
# Raising ConfigEntryAuthFailed will cancel future updates
|
||||
# and start a config flow with SOURCE_REAUTH (async_step_reauth)
|
||||
|
@ -52,7 +56,9 @@ class RingDataCoordinator(DataUpdateCoordinator[RingDevices]):
|
|||
|
||||
async def _async_update_data(self) -> RingDevices:
|
||||
"""Fetch data from API endpoint."""
|
||||
update_method: str = "update_data" if self.first_call else "update_devices"
|
||||
update_method: str = (
|
||||
"async_update_data" if self.first_call else "async_update_devices"
|
||||
)
|
||||
await _call_api(self.hass, getattr(self.ring_api, update_method))
|
||||
self.first_call = False
|
||||
devices: RingDevices = self.ring_api.devices()
|
||||
|
@ -67,7 +73,7 @@ class RingDataCoordinator(DataUpdateCoordinator[RingDevices]):
|
|||
tg.create_task(
|
||||
_call_api(
|
||||
self.hass,
|
||||
lambda device: device.history(limit=10),
|
||||
lambda device: device.async_history(limit=10),
|
||||
device,
|
||||
msg_suffix=f" for device {device.name}", # device_id is the mac
|
||||
)
|
||||
|
@ -75,7 +81,7 @@ class RingDataCoordinator(DataUpdateCoordinator[RingDevices]):
|
|||
tg.create_task(
|
||||
_call_api(
|
||||
self.hass,
|
||||
device.update_health_data,
|
||||
device.async_update_health_data,
|
||||
msg_suffix=f" for device {device.name}",
|
||||
)
|
||||
)
|
||||
|
@ -100,4 +106,4 @@ class RingNotificationsCoordinator(DataUpdateCoordinator[None]):
|
|||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Fetch data from API endpoint."""
|
||||
await _call_api(self.hass, self.ring_api.update_dings)
|
||||
await _call_api(self.hass, self.ring_api.async_update_dings)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""Base class for Ring entity."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Coroutine
|
||||
from typing import Any, Concatenate, Generic, cast
|
||||
|
||||
from ring_doorbell import (
|
||||
|
@ -29,25 +29,23 @@ _RingCoordinatorT = TypeVar(
|
|||
|
||||
|
||||
def exception_wrap[_RingBaseEntityT: RingBaseEntity[Any, Any], **_P, _R](
|
||||
func: Callable[Concatenate[_RingBaseEntityT, _P], _R],
|
||||
) -> Callable[Concatenate[_RingBaseEntityT, _P], _R]:
|
||||
async_func: Callable[Concatenate[_RingBaseEntityT, _P], Coroutine[Any, Any, _R]],
|
||||
) -> Callable[Concatenate[_RingBaseEntityT, _P], Coroutine[Any, Any, _R]]:
|
||||
"""Define a wrapper to catch exceptions and raise HomeAssistant errors."""
|
||||
|
||||
def _wrap(self: _RingBaseEntityT, *args: _P.args, **kwargs: _P.kwargs) -> _R:
|
||||
async def _wrap(self: _RingBaseEntityT, *args: _P.args, **kwargs: _P.kwargs) -> _R:
|
||||
try:
|
||||
return func(self, *args, **kwargs)
|
||||
return await async_func(self, *args, **kwargs)
|
||||
except AuthenticationError as err:
|
||||
self.hass.loop.call_soon_threadsafe(
|
||||
self.coordinator.config_entry.async_start_reauth, self.hass
|
||||
)
|
||||
self.coordinator.config_entry.async_start_reauth(self.hass)
|
||||
raise HomeAssistantError(err) from err
|
||||
except RingTimeout as err:
|
||||
raise HomeAssistantError(
|
||||
f"Timeout communicating with API {func}: {err}"
|
||||
f"Timeout communicating with API {async_func}: {err}"
|
||||
) from err
|
||||
except RingError as err:
|
||||
raise HomeAssistantError(
|
||||
f"Error communicating with API{func}: {err}"
|
||||
f"Error communicating with API{async_func}: {err}"
|
||||
) from err
|
||||
|
||||
return _wrap
|
||||
|
|
|
@ -80,18 +80,18 @@ class RingLight(RingEntity[RingStickUpCam], LightEntity):
|
|||
super()._handle_coordinator_update()
|
||||
|
||||
@exception_wrap
|
||||
def _set_light(self, new_state: OnOffState) -> None:
|
||||
async def _async_set_light(self, new_state: OnOffState) -> None:
|
||||
"""Update light state, and causes Home Assistant to correctly update."""
|
||||
self._device.lights = new_state
|
||||
await self._device.async_set_lights(new_state)
|
||||
|
||||
self._attr_is_on = new_state == OnOffState.ON
|
||||
self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY
|
||||
self.schedule_update_ha_state()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the light on for 30 seconds."""
|
||||
self._set_light(OnOffState.ON)
|
||||
await self._async_set_light(OnOffState.ON)
|
||||
|
||||
def turn_off(self, **kwargs: Any) -> None:
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the light off."""
|
||||
self._set_light(OnOffState.OFF)
|
||||
await self._async_set_light(OnOffState.OFF)
|
||||
|
|
|
@ -14,5 +14,5 @@
|
|||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ring_doorbell"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["ring-doorbell[listen]==0.8.12"]
|
||||
"requirements": ["ring-doorbell[listen]==0.9.0"]
|
||||
}
|
||||
|
|
|
@ -47,8 +47,8 @@ class RingChimeSiren(RingEntity[RingChime], SirenEntity):
|
|||
self._attr_unique_id = f"{self._device.id}-siren"
|
||||
|
||||
@exception_wrap
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Play the test sound on a Ring Chime device."""
|
||||
tone = kwargs.get(ATTR_TONE) or RingEventKind.DING.value
|
||||
|
||||
self._device.test_sound(kind=tone)
|
||||
await self._device.async_test_sound(kind=tone)
|
||||
|
|
|
@ -81,18 +81,18 @@ class SirenSwitch(BaseRingSwitch):
|
|||
super()._handle_coordinator_update()
|
||||
|
||||
@exception_wrap
|
||||
def _set_switch(self, new_state: int) -> None:
|
||||
async def _async_set_switch(self, new_state: int) -> None:
|
||||
"""Update switch state, and causes Home Assistant to correctly update."""
|
||||
self._device.siren = new_state
|
||||
await self._device.async_set_siren(new_state)
|
||||
|
||||
self._attr_is_on = new_state > 0
|
||||
self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY
|
||||
self.schedule_update_ha_state()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the siren on for 30 seconds."""
|
||||
self._set_switch(1)
|
||||
await self._async_set_switch(1)
|
||||
|
||||
def turn_off(self, **kwargs: Any) -> None:
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the siren off."""
|
||||
self._set_switch(0)
|
||||
await self._async_set_switch(0)
|
||||
|
|
|
@ -2507,7 +2507,7 @@ rfk101py==0.0.1
|
|||
rflink==0.0.66
|
||||
|
||||
# homeassistant.components.ring
|
||||
ring-doorbell[listen]==0.8.12
|
||||
ring-doorbell[listen]==0.9.0
|
||||
|
||||
# homeassistant.components.fleetgo
|
||||
ritassist==0.9.2
|
||||
|
|
|
@ -1986,7 +1986,7 @@ reolink-aio==0.9.7
|
|||
rflink==0.0.66
|
||||
|
||||
# homeassistant.components.ring
|
||||
ring-doorbell[listen]==0.8.12
|
||||
ring-doorbell[listen]==0.9.0
|
||||
|
||||
# homeassistant.components.roku
|
||||
rokuecp==0.19.3
|
||||
|
|
|
@ -26,13 +26,23 @@ def mock_setup_entry() -> Generator[AsyncMock]:
|
|||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ring_init_auth_class():
|
||||
"""Mock ring_doorbell.Auth in init and return the mock class."""
|
||||
with patch("homeassistant.components.ring.Auth", autospec=True) as mock_ring_auth:
|
||||
mock_ring_auth.return_value.async_fetch_token.return_value = {
|
||||
"access_token": "mock-token"
|
||||
}
|
||||
yield mock_ring_auth
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ring_auth():
|
||||
"""Mock ring_doorbell.Auth."""
|
||||
with patch(
|
||||
"homeassistant.components.ring.config_flow.Auth", autospec=True
|
||||
) as mock_ring_auth:
|
||||
mock_ring_auth.return_value.fetch_token.return_value = {
|
||||
mock_ring_auth.return_value.async_fetch_token.return_value = {
|
||||
"access_token": "mock-token"
|
||||
}
|
||||
yield mock_ring_auth.return_value
|
||||
|
|
|
@ -10,7 +10,7 @@ Mocks the api calls on the devices such as history() and health().
|
|||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
from time import time
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from ring_doorbell import (
|
||||
RingCapability,
|
||||
|
@ -132,18 +132,18 @@ def _mocked_ring_device(device_dict, device_family, device_class, capabilities):
|
|||
|
||||
# Configure common methods
|
||||
mock_device.has_capability.side_effect = has_capability
|
||||
mock_device.update_health_data.side_effect = lambda: update_health_data(
|
||||
mock_device.async_update_health_data.side_effect = lambda: update_health_data(
|
||||
DOORBOT_HEALTH if device_family != "chimes" else CHIME_HEALTH
|
||||
)
|
||||
# Configure methods based on capability
|
||||
if has_capability(RingCapability.HISTORY):
|
||||
mock_device.configure_mock(last_history=[])
|
||||
mock_device.history.side_effect = lambda *_, **__: update_history_data(
|
||||
mock_device.async_history.side_effect = lambda *_, **__: update_history_data(
|
||||
DOORBOT_HISTORY if device_family != "other" else INTERCOM_HISTORY
|
||||
)
|
||||
|
||||
if has_capability(RingCapability.VIDEO):
|
||||
mock_device.recording_url = MagicMock(return_value="http://dummy.url")
|
||||
mock_device.async_recording_url = AsyncMock(return_value="http://dummy.url")
|
||||
|
||||
if has_capability(RingCapability.MOTION_DETECTION):
|
||||
mock_device.configure_mock(
|
||||
|
|
|
@ -28,11 +28,11 @@ async def test_button_opens_door(
|
|||
await setup_platform(hass, Platform.BUTTON)
|
||||
|
||||
mock_intercom = mock_ring_devices.get_device(185036587)
|
||||
mock_intercom.open_door.assert_not_called()
|
||||
mock_intercom.async_open_door.assert_not_called()
|
||||
|
||||
await hass.services.async_call(
|
||||
"button", "press", {"entity_id": "button.ingress_open_door"}, blocking=True
|
||||
)
|
||||
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
mock_intercom.open_door.assert_called_once()
|
||||
mock_intercom.async_open_door.assert_called_once()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""The tests for the Ring switch platform."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from aiohttp.test_utils import make_mocked_request
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
|
@ -180,8 +180,7 @@ async def test_motion_detection_errors_when_turned_on(
|
|||
assert not any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
|
||||
|
||||
front_camera_mock = mock_ring_devices.get_device(765432)
|
||||
p = PropertyMock(side_effect=exception_type)
|
||||
type(front_camera_mock).motion_detection = p
|
||||
front_camera_mock.async_set_motion_detection.side_effect = exception_type
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
|
@ -191,7 +190,7 @@ async def test_motion_detection_errors_when_turned_on(
|
|||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
p.assert_called_once()
|
||||
front_camera_mock.async_set_motion_detection.assert_called_once()
|
||||
assert (
|
||||
any(
|
||||
flow
|
||||
|
@ -212,7 +211,7 @@ async def test_camera_handle_mjpeg_stream(
|
|||
await setup_platform(hass, Platform.CAMERA)
|
||||
|
||||
front_camera_mock = mock_ring_devices.get_device(765432)
|
||||
front_camera_mock.recording_url.return_value = None
|
||||
front_camera_mock.async_recording_url.return_value = None
|
||||
|
||||
state = hass.states.get("camera.front")
|
||||
assert state is not None
|
||||
|
@ -220,8 +219,8 @@ async def test_camera_handle_mjpeg_stream(
|
|||
mock_request = make_mocked_request("GET", "/", headers={"token": "x"})
|
||||
|
||||
# history not updated yet
|
||||
front_camera_mock.history.assert_not_called()
|
||||
front_camera_mock.recording_url.assert_not_called()
|
||||
front_camera_mock.async_history.assert_not_called()
|
||||
front_camera_mock.async_recording_url.assert_not_called()
|
||||
stream = await camera.async_get_mjpeg_stream(hass, mock_request, "camera.front")
|
||||
assert stream is None
|
||||
|
||||
|
@ -229,30 +228,30 @@ async def test_camera_handle_mjpeg_stream(
|
|||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
front_camera_mock.history.assert_called_once()
|
||||
front_camera_mock.recording_url.assert_called_once()
|
||||
front_camera_mock.async_history.assert_called_once()
|
||||
front_camera_mock.async_recording_url.assert_called_once()
|
||||
|
||||
stream = await camera.async_get_mjpeg_stream(hass, mock_request, "camera.front")
|
||||
assert stream is None
|
||||
|
||||
# Stop the history updating so we can update the values manually
|
||||
front_camera_mock.history = MagicMock()
|
||||
front_camera_mock.async_history = AsyncMock()
|
||||
front_camera_mock.last_history[0]["recording"]["status"] = "not ready"
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
front_camera_mock.recording_url.assert_called_once()
|
||||
front_camera_mock.async_recording_url.assert_called_once()
|
||||
stream = await camera.async_get_mjpeg_stream(hass, mock_request, "camera.front")
|
||||
assert stream is None
|
||||
|
||||
# If the history id hasn't changed the camera will not check again for the video url
|
||||
# until the FORCE_REFRESH_INTERVAL has passed
|
||||
front_camera_mock.last_history[0]["recording"]["status"] = "ready"
|
||||
front_camera_mock.recording_url = MagicMock(return_value="http://dummy.url")
|
||||
front_camera_mock.async_recording_url = AsyncMock(return_value="http://dummy.url")
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
front_camera_mock.recording_url.assert_not_called()
|
||||
front_camera_mock.async_recording_url.assert_not_called()
|
||||
|
||||
stream = await camera.async_get_mjpeg_stream(hass, mock_request, "camera.front")
|
||||
assert stream is None
|
||||
|
@ -260,7 +259,7 @@ async def test_camera_handle_mjpeg_stream(
|
|||
freezer.tick(FORCE_REFRESH_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
front_camera_mock.recording_url.assert_called_once()
|
||||
front_camera_mock.async_recording_url.assert_called_once()
|
||||
|
||||
# Now the stream should be returned
|
||||
stream_reader = MockStreamReader(SMALLEST_VALID_JPEG_BYTES)
|
||||
|
@ -290,8 +289,8 @@ async def test_camera_image(
|
|||
assert state is not None
|
||||
|
||||
# history not updated yet
|
||||
front_camera_mock.history.assert_not_called()
|
||||
front_camera_mock.recording_url.assert_not_called()
|
||||
front_camera_mock.async_history.assert_not_called()
|
||||
front_camera_mock.async_recording_url.assert_not_called()
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.ring.camera.ffmpeg.async_get_image",
|
||||
|
@ -305,8 +304,8 @@ async def test_camera_image(
|
|||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
# history updated so image available
|
||||
front_camera_mock.history.assert_called_once()
|
||||
front_camera_mock.recording_url.assert_called_once()
|
||||
front_camera_mock.async_history.assert_called_once()
|
||||
front_camera_mock.async_recording_url.assert_called_once()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.ring.camera.ffmpeg.async_get_image",
|
||||
|
|
|
@ -57,7 +57,7 @@ async def test_form_error(
|
|||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
mock_ring_auth.fetch_token.side_effect = error_type
|
||||
mock_ring_auth.async_fetch_token.side_effect = error_type
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"username": "hello@home-assistant.io", "password": "test-password"},
|
||||
|
@ -79,7 +79,7 @@ async def test_form_2fa(
|
|||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
mock_ring_auth.fetch_token.side_effect = ring_doorbell.Requires2FAError
|
||||
mock_ring_auth.async_fetch_token.side_effect = ring_doorbell.Requires2FAError
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
|
@ -88,20 +88,20 @@ async def test_form_2fa(
|
|||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_ring_auth.fetch_token.assert_called_once_with(
|
||||
mock_ring_auth.async_fetch_token.assert_called_once_with(
|
||||
"foo@bar.com", "fake-password", None
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "2fa"
|
||||
mock_ring_auth.fetch_token.reset_mock(side_effect=True)
|
||||
mock_ring_auth.fetch_token.return_value = "new-foobar"
|
||||
mock_ring_auth.async_fetch_token.reset_mock(side_effect=True)
|
||||
mock_ring_auth.async_fetch_token.return_value = "new-foobar"
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
user_input={"2fa": "123456"},
|
||||
)
|
||||
|
||||
mock_ring_auth.fetch_token.assert_called_once_with(
|
||||
mock_ring_auth.async_fetch_token.assert_called_once_with(
|
||||
"foo@bar.com", "fake-password", "123456"
|
||||
)
|
||||
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
@ -128,7 +128,7 @@ async def test_reauth(
|
|||
[result] = flows
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
mock_ring_auth.fetch_token.side_effect = ring_doorbell.Requires2FAError
|
||||
mock_ring_auth.async_fetch_token.side_effect = ring_doorbell.Requires2FAError
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
|
@ -136,19 +136,19 @@ async def test_reauth(
|
|||
},
|
||||
)
|
||||
|
||||
mock_ring_auth.fetch_token.assert_called_once_with(
|
||||
mock_ring_auth.async_fetch_token.assert_called_once_with(
|
||||
"foo@bar.com", "other_fake_password", None
|
||||
)
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "2fa"
|
||||
mock_ring_auth.fetch_token.reset_mock(side_effect=True)
|
||||
mock_ring_auth.fetch_token.return_value = "new-foobar"
|
||||
mock_ring_auth.async_fetch_token.reset_mock(side_effect=True)
|
||||
mock_ring_auth.async_fetch_token.return_value = "new-foobar"
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
user_input={"2fa": "123456"},
|
||||
)
|
||||
|
||||
mock_ring_auth.fetch_token.assert_called_once_with(
|
||||
mock_ring_auth.async_fetch_token.assert_called_once_with(
|
||||
"foo@bar.com", "other_fake_password", "123456"
|
||||
)
|
||||
assert result3["type"] is FlowResultType.ABORT
|
||||
|
@ -185,7 +185,7 @@ async def test_reauth_error(
|
|||
[result] = flows
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
mock_ring_auth.fetch_token.side_effect = error_type
|
||||
mock_ring_auth.async_fetch_token.side_effect = error_type
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
|
@ -194,15 +194,15 @@ async def test_reauth_error(
|
|||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_ring_auth.fetch_token.assert_called_once_with(
|
||||
mock_ring_auth.async_fetch_token.assert_called_once_with(
|
||||
"foo@bar.com", "error_fake_password", None
|
||||
)
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": errors_msg}
|
||||
|
||||
# Now test reauth can go on to succeed
|
||||
mock_ring_auth.fetch_token.reset_mock(side_effect=True)
|
||||
mock_ring_auth.fetch_token.return_value = "new-foobar"
|
||||
mock_ring_auth.async_fetch_token.reset_mock(side_effect=True)
|
||||
mock_ring_auth.async_fetch_token.return_value = "new-foobar"
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
user_input={
|
||||
|
@ -210,7 +210,7 @@ async def test_reauth_error(
|
|||
},
|
||||
)
|
||||
|
||||
mock_ring_auth.fetch_token.assert_called_once_with(
|
||||
mock_ring_auth.async_fetch_token.assert_called_once_with(
|
||||
"foo@bar.com", "other_fake_password", None
|
||||
)
|
||||
assert result3["type"] is FlowResultType.ABORT
|
||||
|
|
|
@ -10,7 +10,7 @@ from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
|||
from homeassistant.components.ring import DOMAIN
|
||||
from homeassistant.components.ring.const import SCAN_INTERVAL
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.const import CONF_USERNAME
|
||||
from homeassistant.const import CONF_TOKEN, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
@ -42,11 +42,11 @@ async def test_setup_entry_device_update(
|
|||
"""Test devices are updating after setup entry."""
|
||||
|
||||
front_door_doorbell = mock_ring_devices.get_device(987654)
|
||||
front_door_doorbell.history.assert_not_called()
|
||||
front_door_doorbell.async_history.assert_not_called()
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
front_door_doorbell.history.assert_called_once()
|
||||
front_door_doorbell.async_history.assert_called_once()
|
||||
|
||||
|
||||
async def test_auth_failed_on_setup(
|
||||
|
@ -56,7 +56,7 @@ async def test_auth_failed_on_setup(
|
|||
) -> None:
|
||||
"""Test auth failure on setup entry."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
mock_ring_client.update_data.side_effect = AuthenticationError
|
||||
mock_ring_client.async_update_data.side_effect = AuthenticationError
|
||||
|
||||
assert not any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
@ -90,7 +90,7 @@ async def test_error_on_setup(
|
|||
"""Test non-auth errors on setup entry."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
mock_ring_client.update_data.side_effect = error_type
|
||||
mock_ring_client.async_update_data.side_effect = error_type
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
@ -113,7 +113,7 @@ async def test_auth_failure_on_global_update(
|
|||
await hass.async_block_till_done()
|
||||
assert not any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
|
||||
|
||||
mock_ring_client.update_devices.side_effect = AuthenticationError
|
||||
mock_ring_client.async_update_devices.side_effect = AuthenticationError
|
||||
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
|
@ -139,7 +139,7 @@ async def test_auth_failure_on_device_update(
|
|||
assert not any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
|
||||
|
||||
front_door_doorbell = mock_ring_devices.get_device(987654)
|
||||
front_door_doorbell.history.side_effect = AuthenticationError
|
||||
front_door_doorbell.async_history.side_effect = AuthenticationError
|
||||
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
|
@ -178,7 +178,7 @@ async def test_error_on_global_update(
|
|||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_ring_client.update_devices.side_effect = error_type
|
||||
mock_ring_client.async_update_devices.side_effect = error_type
|
||||
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
|
@ -219,7 +219,7 @@ async def test_error_on_device_update(
|
|||
await hass.async_block_till_done()
|
||||
|
||||
front_door_doorbell = mock_ring_devices.get_device(765432)
|
||||
front_door_doorbell.history.side_effect = error_type
|
||||
front_door_doorbell.async_history.side_effect = error_type
|
||||
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
|
@ -386,3 +386,30 @@ async def test_update_unique_id_no_update(
|
|||
assert entity_migrated
|
||||
assert entity_migrated.unique_id == correct_unique_id
|
||||
assert "Fixing non string unique id" not in caplog.text
|
||||
|
||||
|
||||
async def test_token_updated(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_ring_client,
|
||||
mock_ring_init_auth_class,
|
||||
) -> None:
|
||||
"""Test that the token value is updated in the config entry.
|
||||
|
||||
This simulates the api calling the callback.
|
||||
"""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
assert mock_ring_init_auth_class.call_count == 1
|
||||
token_updater = mock_ring_init_auth_class.call_args.args[2]
|
||||
assert mock_config_entry.data[CONF_TOKEN] == {"access_token": "mock-token"}
|
||||
|
||||
mock_ring_client.async_update_devices.side_effect = lambda: token_updater(
|
||||
{"access_token": "new-mock-token"}
|
||||
)
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_config_entry.data[CONF_TOKEN] == {"access_token": "new-mock-token"}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
"""The tests for the Ring light platform."""
|
||||
|
||||
from unittest.mock import PropertyMock
|
||||
|
||||
import pytest
|
||||
import ring_doorbell
|
||||
|
||||
|
@ -109,15 +107,14 @@ async def test_light_errors_when_turned_on(
|
|||
assert not any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
|
||||
|
||||
front_light_mock = mock_ring_devices.get_device(765432)
|
||||
p = PropertyMock(side_effect=exception_type)
|
||||
type(front_light_mock).lights = p
|
||||
front_light_mock.async_set_lights.side_effect = exception_type
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
"light", "turn_on", {"entity_id": "light.front_light"}, blocking=True
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
p.assert_called_once()
|
||||
front_light_mock.async_set_lights.assert_called_once()
|
||||
|
||||
assert (
|
||||
any(
|
||||
|
|
|
@ -49,7 +49,7 @@ async def test_default_ding_chime_can_be_played(
|
|||
await hass.async_block_till_done()
|
||||
|
||||
downstairs_chime_mock = mock_ring_devices.get_device(123456)
|
||||
downstairs_chime_mock.test_sound.assert_called_once_with(kind="ding")
|
||||
downstairs_chime_mock.async_test_sound.assert_called_once_with(kind="ding")
|
||||
|
||||
state = hass.states.get("siren.downstairs_siren")
|
||||
assert state.state == "unknown"
|
||||
|
@ -71,7 +71,7 @@ async def test_turn_on_plays_default_chime(
|
|||
await hass.async_block_till_done()
|
||||
|
||||
downstairs_chime_mock = mock_ring_devices.get_device(123456)
|
||||
downstairs_chime_mock.test_sound.assert_called_once_with(kind="ding")
|
||||
downstairs_chime_mock.async_test_sound.assert_called_once_with(kind="ding")
|
||||
|
||||
state = hass.states.get("siren.downstairs_siren")
|
||||
assert state.state == "unknown"
|
||||
|
@ -95,7 +95,7 @@ async def test_explicit_ding_chime_can_be_played(
|
|||
await hass.async_block_till_done()
|
||||
|
||||
downstairs_chime_mock = mock_ring_devices.get_device(123456)
|
||||
downstairs_chime_mock.test_sound.assert_called_once_with(kind="ding")
|
||||
downstairs_chime_mock.async_test_sound.assert_called_once_with(kind="ding")
|
||||
|
||||
state = hass.states.get("siren.downstairs_siren")
|
||||
assert state.state == "unknown"
|
||||
|
@ -117,7 +117,7 @@ async def test_motion_chime_can_be_played(
|
|||
await hass.async_block_till_done()
|
||||
|
||||
downstairs_chime_mock = mock_ring_devices.get_device(123456)
|
||||
downstairs_chime_mock.test_sound.assert_called_once_with(kind="motion")
|
||||
downstairs_chime_mock.async_test_sound.assert_called_once_with(kind="motion")
|
||||
|
||||
state = hass.states.get("siren.downstairs_siren")
|
||||
assert state.state == "unknown"
|
||||
|
@ -146,7 +146,7 @@ async def test_siren_errors_when_turned_on(
|
|||
assert not any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
|
||||
|
||||
downstairs_chime_mock = mock_ring_devices.get_device(123456)
|
||||
downstairs_chime_mock.test_sound.side_effect = exception_type
|
||||
downstairs_chime_mock.async_test_sound.side_effect = exception_type
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
|
@ -155,7 +155,8 @@ async def test_siren_errors_when_turned_on(
|
|||
{"entity_id": "siren.downstairs_siren", "tone": "motion"},
|
||||
blocking=True,
|
||||
)
|
||||
downstairs_chime_mock.test_sound.assert_called_once_with(kind="motion")
|
||||
downstairs_chime_mock.async_test_sound.assert_called_once_with(kind="motion")
|
||||
await hass.async_block_till_done()
|
||||
assert (
|
||||
any(
|
||||
flow
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
"""The tests for the Ring switch platform."""
|
||||
|
||||
from unittest.mock import PropertyMock
|
||||
|
||||
import pytest
|
||||
import ring_doorbell
|
||||
|
||||
|
@ -116,15 +114,14 @@ async def test_switch_errors_when_turned_on(
|
|||
assert not any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
|
||||
|
||||
front_siren_mock = mock_ring_devices.get_device(765432)
|
||||
p = PropertyMock(side_effect=exception_type)
|
||||
type(front_siren_mock).siren = p
|
||||
front_siren_mock.async_set_siren.side_effect = exception_type
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
"switch", "turn_on", {"entity_id": "switch.front_siren"}, blocking=True
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
p.assert_called_once()
|
||||
front_siren_mock.async_set_siren.assert_called_once()
|
||||
assert (
|
||||
any(
|
||||
flow
|
||||
|
|
Loading…
Add table
Reference in a new issue