From e26d363b5ef1c32358c33212c6cf85b627e6e66e Mon Sep 17 00:00:00 2001 From: "Steven B." <51370195+sdb9696@users.noreply.github.com> Date: Sat, 24 Aug 2024 07:23:31 +0100 Subject: [PATCH] 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 --- homeassistant/components/ring/__init__.py | 18 ++++---- homeassistant/components/ring/button.py | 4 +- homeassistant/components/ring/camera.py | 20 ++++----- homeassistant/components/ring/config_flow.py | 3 +- homeassistant/components/ring/coordinator.py | 20 ++++++--- homeassistant/components/ring/entity.py | 18 ++++---- homeassistant/components/ring/light.py | 14 +++--- homeassistant/components/ring/manifest.json | 2 +- homeassistant/components/ring/siren.py | 4 +- homeassistant/components/ring/switch.py | 14 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ring/conftest.py | 12 +++++- tests/components/ring/device_mocks.py | 8 ++-- tests/components/ring/test_button.py | 4 +- tests/components/ring/test_camera.py | 35 ++++++++------- tests/components/ring/test_config_flow.py | 32 +++++++------- tests/components/ring/test_init.py | 45 ++++++++++++++++---- tests/components/ring/test_light.py | 7 +-- tests/components/ring/test_siren.py | 13 +++--- tests/components/ring/test_switch.py | 7 +-- 21 files changed, 159 insertions(+), 125 deletions(-) diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 36c66550ddc..14ab435fda6 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -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) diff --git a/homeassistant/components/ring/button.py b/homeassistant/components/ring/button.py index 15d56a8b7cf..c8d7d902d18 100644 --- a/homeassistant/components/ring/button.py +++ b/homeassistant/components/ring/button.py @@ -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() diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index ba75b68434d..b45803f3618 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -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) diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index 6239105580d..ee78541dec7 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -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), diff --git a/homeassistant/components/ring/coordinator.py b/homeassistant/components/ring/coordinator.py index 1a52fc78988..600743005eb 100644 --- a/homeassistant/components/ring/coordinator.py +++ b/homeassistant/components/ring/coordinator.py @@ -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) diff --git a/homeassistant/components/ring/entity.py b/homeassistant/components/ring/entity.py index a4275815450..72deb09b76f 100644 --- a/homeassistant/components/ring/entity.py +++ b/homeassistant/components/ring/entity.py @@ -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 diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index 5747c9e77f7..f7f7f9b44ae 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -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) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index a3d15bd711d..23e7b882efe 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -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"] } diff --git a/homeassistant/components/ring/siren.py b/homeassistant/components/ring/siren.py index f63f9d33182..665de07a5bb 100644 --- a/homeassistant/components/ring/siren.py +++ b/homeassistant/components/ring/siren.py @@ -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) diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 0e032907bae..810011d68c8 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -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) diff --git a/requirements_all.txt b/requirements_all.txt index 3a2054b4b6d..d2f63ec9ee7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec8059c4e6f..b246cb714f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py index cd4447c1a9a..4456a9daa26 100644 --- a/tests/components/ring/conftest.py +++ b/tests/components/ring/conftest.py @@ -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 diff --git a/tests/components/ring/device_mocks.py b/tests/components/ring/device_mocks.py index 88ad37bdd36..d2671c3896d 100644 --- a/tests/components/ring/device_mocks.py +++ b/tests/components/ring/device_mocks.py @@ -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( diff --git a/tests/components/ring/test_button.py b/tests/components/ring/test_button.py index 6fef3295159..946a893c8ad 100644 --- a/tests/components/ring/test_button.py +++ b/tests/components/ring/test_button.py @@ -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() diff --git a/tests/components/ring/test_camera.py b/tests/components/ring/test_camera.py index 49b7dc10f05..619fb52846c 100644 --- a/tests/components/ring/test_camera.py +++ b/tests/components/ring/test_camera.py @@ -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", diff --git a/tests/components/ring/test_config_flow.py b/tests/components/ring/test_config_flow.py index 2420bb9cc50..bbaec2e37c4 100644 --- a/tests/components/ring/test_config_flow.py +++ b/tests/components/ring/test_config_flow.py @@ -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 diff --git a/tests/components/ring/test_init.py b/tests/components/ring/test_init.py index d8529e874b9..4ab3e1bd366 100644 --- a/tests/components/ring/test_init.py +++ b/tests/components/ring/test_init.py @@ -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"} diff --git a/tests/components/ring/test_light.py b/tests/components/ring/test_light.py index c2d21a22951..22ed4a31cf8 100644 --- a/tests/components/ring/test_light.py +++ b/tests/components/ring/test_light.py @@ -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( diff --git a/tests/components/ring/test_siren.py b/tests/components/ring/test_siren.py index 695b54c3971..e71dd1e6e77 100644 --- a/tests/components/ring/test_siren.py +++ b/tests/components/ring/test_siren.py @@ -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 diff --git a/tests/components/ring/test_switch.py b/tests/components/ring/test_switch.py index 405f20420b7..f7aa885342a 100644 --- a/tests/components/ring/test_switch.py +++ b/tests/components/ring/test_switch.py @@ -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