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:
Steven B. 2024-08-24 07:23:31 +01:00 committed by GitHub
parent 7ae8f4c9d0
commit e26d363b5e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 159 additions and 125 deletions

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -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),

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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"]
}

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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(

View file

@ -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()

View file

@ -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",

View file

@ -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

View file

@ -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"}

View file

@ -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(

View file

@ -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

View file

@ -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