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 __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from functools import partial
import logging import logging
from typing import Any, cast 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.const import APPLICATION_NAME, CONF_TOKEN, __version__
from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er 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 homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from .const import DOMAIN, PLATFORMS from .const import DOMAIN, PLATFORMS
@ -35,17 +35,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry.""" """Set up a config entry."""
def token_updater(token: dict[str, Any]) -> None: def token_updater(token: dict[str, Any]) -> None:
"""Handle from sync context when token is updated.""" """Handle from async context when token is updated."""
hass.loop.call_soon_threadsafe( hass.config_entries.async_update_entry(
partial(
hass.config_entries.async_update_entry,
entry, entry,
data={**entry.data, CONF_TOKEN: token}, data={**entry.data, CONF_TOKEN: token},
) )
)
auth = Auth( 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) ring = Ring(auth)

View File

@ -53,6 +53,6 @@ class RingDoorButton(RingEntity[RingOther], ButtonEntity):
self._attr_unique_id = f"{device.id}-{description.key}" self._attr_unique_id = f"{device.id}-{description.key}"
@exception_wrap @exception_wrap
def press(self) -> None: async def async_press(self) -> None:
"""Open the door.""" """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"]: if self._last_video_id != self._last_event["id"]:
self._image = None 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._last_video_id = self._last_event["id"]
self._expires_at = FORCE_REFRESH_INTERVAL + utcnow self._expires_at = FORCE_REFRESH_INTERVAL + utcnow
@exception_wrap @exception_wrap
def _get_video(self) -> str | None: async def _async_get_video(self) -> str | None:
if TYPE_CHECKING: if TYPE_CHECKING:
# _last_event is set before calling update so will never be None # _last_event is set before calling update so will never be None
assert self._last_event assert self._last_event
event_id = self._last_event.get("id") event_id = self._last_event.get("id")
assert event_id and isinstance(event_id, int) 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 @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): if not self._device.has_capability(MOTION_DETECTION_CAPABILITY):
_LOGGER.error( _LOGGER.error(
"Entity %s does not have motion detection capability", self.entity_id "Entity %s does not have motion detection capability", self.entity_id
) )
return return
self._device.motion_detection = new_state await self._device.async_set_motion_detection(new_state)
self._attr_motion_detection_enabled = 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.""" """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.""" """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}") auth = Auth(f"{APPLICATION_NAME}/{ha_version}")
try: try:
token = await hass.async_add_executor_job( token = await auth.async_fetch_token(
auth.fetch_token,
data[CONF_USERNAME], data[CONF_USERNAME],
data[CONF_PASSWORD], data[CONF_PASSWORD],
data.get(CONF_2FA), data.get(CONF_2FA),

View File

@ -1,8 +1,9 @@
"""Data coordinators for the ring integration.""" """Data coordinators for the ring integration."""
from asyncio import TaskGroup from asyncio import TaskGroup
from collections.abc import Callable from collections.abc import Callable, Coroutine
import logging import logging
from typing import Any
from ring_doorbell import AuthenticationError, Ring, RingDevices, RingError, RingTimeout from ring_doorbell import AuthenticationError, Ring, RingDevices, RingError, RingTimeout
@ -16,10 +17,13 @@ _LOGGER = logging.getLogger(__name__)
async def _call_api[*_Ts, _R]( 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: ) -> _R:
try: try:
return await hass.async_add_executor_job(target, *args) return await target(*args)
except AuthenticationError as err: except AuthenticationError as err:
# Raising ConfigEntryAuthFailed will cancel future updates # Raising ConfigEntryAuthFailed will cancel future updates
# and start a config flow with SOURCE_REAUTH (async_step_reauth) # 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: async def _async_update_data(self) -> RingDevices:
"""Fetch data from API endpoint.""" """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)) await _call_api(self.hass, getattr(self.ring_api, update_method))
self.first_call = False self.first_call = False
devices: RingDevices = self.ring_api.devices() devices: RingDevices = self.ring_api.devices()
@ -67,7 +73,7 @@ class RingDataCoordinator(DataUpdateCoordinator[RingDevices]):
tg.create_task( tg.create_task(
_call_api( _call_api(
self.hass, self.hass,
lambda device: device.history(limit=10), lambda device: device.async_history(limit=10),
device, device,
msg_suffix=f" for device {device.name}", # device_id is the mac msg_suffix=f" for device {device.name}", # device_id is the mac
) )
@ -75,7 +81,7 @@ class RingDataCoordinator(DataUpdateCoordinator[RingDevices]):
tg.create_task( tg.create_task(
_call_api( _call_api(
self.hass, self.hass,
device.update_health_data, device.async_update_health_data,
msg_suffix=f" for device {device.name}", msg_suffix=f" for device {device.name}",
) )
) )
@ -100,4 +106,4 @@ class RingNotificationsCoordinator(DataUpdateCoordinator[None]):
async def _async_update_data(self) -> None: async def _async_update_data(self) -> None:
"""Fetch data from API endpoint.""" """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.""" """Base class for Ring entity."""
from collections.abc import Callable from collections.abc import Callable, Coroutine
from typing import Any, Concatenate, Generic, cast from typing import Any, Concatenate, Generic, cast
from ring_doorbell import ( from ring_doorbell import (
@ -29,25 +29,23 @@ _RingCoordinatorT = TypeVar(
def exception_wrap[_RingBaseEntityT: RingBaseEntity[Any, Any], **_P, _R]( def exception_wrap[_RingBaseEntityT: RingBaseEntity[Any, Any], **_P, _R](
func: Callable[Concatenate[_RingBaseEntityT, _P], _R], async_func: Callable[Concatenate[_RingBaseEntityT, _P], Coroutine[Any, Any, _R]],
) -> Callable[Concatenate[_RingBaseEntityT, _P], _R]: ) -> Callable[Concatenate[_RingBaseEntityT, _P], Coroutine[Any, Any, _R]]:
"""Define a wrapper to catch exceptions and raise HomeAssistant errors.""" """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: try:
return func(self, *args, **kwargs) return await async_func(self, *args, **kwargs)
except AuthenticationError as err: 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 raise HomeAssistantError(err) from err
except RingTimeout as err: except RingTimeout as err:
raise HomeAssistantError( raise HomeAssistantError(
f"Timeout communicating with API {func}: {err}" f"Timeout communicating with API {async_func}: {err}"
) from err ) from err
except RingError as err: except RingError as err:
raise HomeAssistantError( raise HomeAssistantError(
f"Error communicating with API{func}: {err}" f"Error communicating with API{async_func}: {err}"
) from err ) from err
return _wrap return _wrap

View File

@ -80,18 +80,18 @@ class RingLight(RingEntity[RingStickUpCam], LightEntity):
super()._handle_coordinator_update() super()._handle_coordinator_update()
@exception_wrap @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.""" """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._attr_is_on = new_state == OnOffState.ON
self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY 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.""" """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.""" """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", "iot_class": "cloud_polling",
"loggers": ["ring_doorbell"], "loggers": ["ring_doorbell"],
"quality_scale": "silver", "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" self._attr_unique_id = f"{self._device.id}-siren"
@exception_wrap @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.""" """Play the test sound on a Ring Chime device."""
tone = kwargs.get(ATTR_TONE) or RingEventKind.DING.value 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() super()._handle_coordinator_update()
@exception_wrap @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.""" """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._attr_is_on = new_state > 0
self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY 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.""" """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.""" """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 rflink==0.0.66
# homeassistant.components.ring # homeassistant.components.ring
ring-doorbell[listen]==0.8.12 ring-doorbell[listen]==0.9.0
# homeassistant.components.fleetgo # homeassistant.components.fleetgo
ritassist==0.9.2 ritassist==0.9.2

View File

@ -1986,7 +1986,7 @@ reolink-aio==0.9.7
rflink==0.0.66 rflink==0.0.66
# homeassistant.components.ring # homeassistant.components.ring
ring-doorbell[listen]==0.8.12 ring-doorbell[listen]==0.9.0
# homeassistant.components.roku # homeassistant.components.roku
rokuecp==0.19.3 rokuecp==0.19.3

View File

@ -26,13 +26,23 @@ def mock_setup_entry() -> Generator[AsyncMock]:
yield mock_setup_entry 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 @pytest.fixture
def mock_ring_auth(): def mock_ring_auth():
"""Mock ring_doorbell.Auth.""" """Mock ring_doorbell.Auth."""
with patch( with patch(
"homeassistant.components.ring.config_flow.Auth", autospec=True "homeassistant.components.ring.config_flow.Auth", autospec=True
) as mock_ring_auth: ) 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" "access_token": "mock-token"
} }
yield mock_ring_auth.return_value 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 copy import deepcopy
from datetime import datetime from datetime import datetime
from time import time from time import time
from unittest.mock import MagicMock from unittest.mock import AsyncMock, MagicMock
from ring_doorbell import ( from ring_doorbell import (
RingCapability, RingCapability,
@ -132,18 +132,18 @@ def _mocked_ring_device(device_dict, device_family, device_class, capabilities):
# Configure common methods # Configure common methods
mock_device.has_capability.side_effect = has_capability 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 DOORBOT_HEALTH if device_family != "chimes" else CHIME_HEALTH
) )
# Configure methods based on capability # Configure methods based on capability
if has_capability(RingCapability.HISTORY): if has_capability(RingCapability.HISTORY):
mock_device.configure_mock(last_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 DOORBOT_HISTORY if device_family != "other" else INTERCOM_HISTORY
) )
if has_capability(RingCapability.VIDEO): 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): if has_capability(RingCapability.MOTION_DETECTION):
mock_device.configure_mock( mock_device.configure_mock(

View File

@ -28,11 +28,11 @@ async def test_button_opens_door(
await setup_platform(hass, Platform.BUTTON) await setup_platform(hass, Platform.BUTTON)
mock_intercom = mock_ring_devices.get_device(185036587) 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( await hass.services.async_call(
"button", "press", {"entity_id": "button.ingress_open_door"}, blocking=True "button", "press", {"entity_id": "button.ingress_open_door"}, blocking=True
) )
await hass.async_block_till_done(wait_background_tasks=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.""" """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 aiohttp.test_utils import make_mocked_request
from freezegun.api import FrozenDateTimeFactory 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})) assert not any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
front_camera_mock = mock_ring_devices.get_device(765432) front_camera_mock = mock_ring_devices.get_device(765432)
p = PropertyMock(side_effect=exception_type) front_camera_mock.async_set_motion_detection.side_effect = exception_type
type(front_camera_mock).motion_detection = p
with pytest.raises(HomeAssistantError): with pytest.raises(HomeAssistantError):
await hass.services.async_call( await hass.services.async_call(
@ -191,7 +190,7 @@ async def test_motion_detection_errors_when_turned_on(
blocking=True, blocking=True,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
p.assert_called_once() front_camera_mock.async_set_motion_detection.assert_called_once()
assert ( assert (
any( any(
flow flow
@ -212,7 +211,7 @@ async def test_camera_handle_mjpeg_stream(
await setup_platform(hass, Platform.CAMERA) await setup_platform(hass, Platform.CAMERA)
front_camera_mock = mock_ring_devices.get_device(765432) 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") state = hass.states.get("camera.front")
assert state is not None 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"}) mock_request = make_mocked_request("GET", "/", headers={"token": "x"})
# history not updated yet # history not updated yet
front_camera_mock.history.assert_not_called() front_camera_mock.async_history.assert_not_called()
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") stream = await camera.async_get_mjpeg_stream(hass, mock_request, "camera.front")
assert stream is None assert stream is None
@ -229,30 +228,30 @@ async def test_camera_handle_mjpeg_stream(
freezer.tick(SCAN_INTERVAL) freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True) await hass.async_block_till_done(wait_background_tasks=True)
front_camera_mock.history.assert_called_once() front_camera_mock.async_history.assert_called_once()
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") stream = await camera.async_get_mjpeg_stream(hass, mock_request, "camera.front")
assert stream is None assert stream is None
# Stop the history updating so we can update the values manually # 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" front_camera_mock.last_history[0]["recording"]["status"] = "not ready"
freezer.tick(SCAN_INTERVAL) freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True) 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") stream = await camera.async_get_mjpeg_stream(hass, mock_request, "camera.front")
assert stream is None assert stream is None
# If the history id hasn't changed the camera will not check again for the video url # If the history id hasn't changed the camera will not check again for the video url
# until the FORCE_REFRESH_INTERVAL has passed # until the FORCE_REFRESH_INTERVAL has passed
front_camera_mock.last_history[0]["recording"]["status"] = "ready" 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) freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True) 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") stream = await camera.async_get_mjpeg_stream(hass, mock_request, "camera.front")
assert stream is None assert stream is None
@ -260,7 +259,7 @@ async def test_camera_handle_mjpeg_stream(
freezer.tick(FORCE_REFRESH_INTERVAL) freezer.tick(FORCE_REFRESH_INTERVAL)
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True) 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 # Now the stream should be returned
stream_reader = MockStreamReader(SMALLEST_VALID_JPEG_BYTES) stream_reader = MockStreamReader(SMALLEST_VALID_JPEG_BYTES)
@ -290,8 +289,8 @@ async def test_camera_image(
assert state is not None assert state is not None
# history not updated yet # history not updated yet
front_camera_mock.history.assert_not_called() front_camera_mock.async_history.assert_not_called()
front_camera_mock.recording_url.assert_not_called() front_camera_mock.async_recording_url.assert_not_called()
with ( with (
patch( patch(
"homeassistant.components.ring.camera.ffmpeg.async_get_image", "homeassistant.components.ring.camera.ffmpeg.async_get_image",
@ -305,8 +304,8 @@ async def test_camera_image(
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True) await hass.async_block_till_done(wait_background_tasks=True)
# history updated so image available # history updated so image available
front_camera_mock.history.assert_called_once() front_camera_mock.async_history.assert_called_once()
front_camera_mock.recording_url.assert_called_once() front_camera_mock.async_recording_url.assert_called_once()
with patch( with patch(
"homeassistant.components.ring.camera.ffmpeg.async_get_image", "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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} 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( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{"username": "hello@home-assistant.io", "password": "test-password"}, {"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["type"] is FlowResultType.FORM
assert result["errors"] == {} 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( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ {
@ -88,20 +88,20 @@ async def test_form_2fa(
}, },
) )
await hass.async_block_till_done() 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 "foo@bar.com", "fake-password", None
) )
assert result2["type"] is FlowResultType.FORM assert result2["type"] is FlowResultType.FORM
assert result2["step_id"] == "2fa" assert result2["step_id"] == "2fa"
mock_ring_auth.fetch_token.reset_mock(side_effect=True) mock_ring_auth.async_fetch_token.reset_mock(side_effect=True)
mock_ring_auth.fetch_token.return_value = "new-foobar" mock_ring_auth.async_fetch_token.return_value = "new-foobar"
result3 = await hass.config_entries.flow.async_configure( result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"], result2["flow_id"],
user_input={"2fa": "123456"}, 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" "foo@bar.com", "fake-password", "123456"
) )
assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["type"] is FlowResultType.CREATE_ENTRY
@ -128,7 +128,7 @@ async def test_reauth(
[result] = flows [result] = flows
assert result["step_id"] == "reauth_confirm" 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( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input={ 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 "foo@bar.com", "other_fake_password", None
) )
assert result2["type"] is FlowResultType.FORM assert result2["type"] is FlowResultType.FORM
assert result2["step_id"] == "2fa" assert result2["step_id"] == "2fa"
mock_ring_auth.fetch_token.reset_mock(side_effect=True) mock_ring_auth.async_fetch_token.reset_mock(side_effect=True)
mock_ring_auth.fetch_token.return_value = "new-foobar" mock_ring_auth.async_fetch_token.return_value = "new-foobar"
result3 = await hass.config_entries.flow.async_configure( result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"], result2["flow_id"],
user_input={"2fa": "123456"}, 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" "foo@bar.com", "other_fake_password", "123456"
) )
assert result3["type"] is FlowResultType.ABORT assert result3["type"] is FlowResultType.ABORT
@ -185,7 +185,7 @@ async def test_reauth_error(
[result] = flows [result] = flows
assert result["step_id"] == "reauth_confirm" 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( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input={ user_input={
@ -194,15 +194,15 @@ async def test_reauth_error(
) )
await hass.async_block_till_done() 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 "foo@bar.com", "error_fake_password", None
) )
assert result2["type"] is FlowResultType.FORM assert result2["type"] is FlowResultType.FORM
assert result2["errors"] == {"base": errors_msg} assert result2["errors"] == {"base": errors_msg}
# Now test reauth can go on to succeed # Now test reauth can go on to succeed
mock_ring_auth.fetch_token.reset_mock(side_effect=True) mock_ring_auth.async_fetch_token.reset_mock(side_effect=True)
mock_ring_auth.fetch_token.return_value = "new-foobar" mock_ring_auth.async_fetch_token.return_value = "new-foobar"
result3 = await hass.config_entries.flow.async_configure( result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"], result2["flow_id"],
user_input={ 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 "foo@bar.com", "other_fake_password", None
) )
assert result3["type"] is FlowResultType.ABORT 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 import DOMAIN
from homeassistant.components.ring.const import SCAN_INTERVAL from homeassistant.components.ring.const import SCAN_INTERVAL
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState 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.core import HomeAssistant
from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.helpers import entity_registry as er, issue_registry as ir
from homeassistant.setup import async_setup_component 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.""" """Test devices are updating after setup entry."""
front_door_doorbell = mock_ring_devices.get_device(987654) 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) freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True) 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( async def test_auth_failed_on_setup(
@ -56,7 +56,7 @@ async def test_auth_failed_on_setup(
) -> None: ) -> None:
"""Test auth failure on setup entry.""" """Test auth failure on setup entry."""
mock_config_entry.add_to_hass(hass) 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})) assert not any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
await hass.config_entries.async_setup(mock_config_entry.entry_id) 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.""" """Test non-auth errors on setup entry."""
mock_config_entry.add_to_hass(hass) 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.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -113,7 +113,7 @@ async def test_auth_failure_on_global_update(
await hass.async_block_till_done() await hass.async_block_till_done()
assert not any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) 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) freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass) 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})) 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 = 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) freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass) 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.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done() 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) freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass) async_fire_time_changed(hass)
@ -219,7 +219,7 @@ async def test_error_on_device_update(
await hass.async_block_till_done() await hass.async_block_till_done()
front_door_doorbell = mock_ring_devices.get_device(765432) 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) freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass) async_fire_time_changed(hass)
@ -386,3 +386,30 @@ async def test_update_unique_id_no_update(
assert entity_migrated assert entity_migrated
assert entity_migrated.unique_id == correct_unique_id assert entity_migrated.unique_id == correct_unique_id
assert "Fixing non string unique id" not in caplog.text 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.""" """The tests for the Ring light platform."""
from unittest.mock import PropertyMock
import pytest import pytest
import ring_doorbell 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})) assert not any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
front_light_mock = mock_ring_devices.get_device(765432) front_light_mock = mock_ring_devices.get_device(765432)
p = PropertyMock(side_effect=exception_type) front_light_mock.async_set_lights.side_effect = exception_type
type(front_light_mock).lights = p
with pytest.raises(HomeAssistantError): with pytest.raises(HomeAssistantError):
await hass.services.async_call( await hass.services.async_call(
"light", "turn_on", {"entity_id": "light.front_light"}, blocking=True "light", "turn_on", {"entity_id": "light.front_light"}, blocking=True
) )
await hass.async_block_till_done() await hass.async_block_till_done()
p.assert_called_once() front_light_mock.async_set_lights.assert_called_once()
assert ( assert (
any( any(

View File

@ -49,7 +49,7 @@ async def test_default_ding_chime_can_be_played(
await hass.async_block_till_done() await hass.async_block_till_done()
downstairs_chime_mock = mock_ring_devices.get_device(123456) 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") state = hass.states.get("siren.downstairs_siren")
assert state.state == "unknown" assert state.state == "unknown"
@ -71,7 +71,7 @@ async def test_turn_on_plays_default_chime(
await hass.async_block_till_done() await hass.async_block_till_done()
downstairs_chime_mock = mock_ring_devices.get_device(123456) 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") state = hass.states.get("siren.downstairs_siren")
assert state.state == "unknown" assert state.state == "unknown"
@ -95,7 +95,7 @@ async def test_explicit_ding_chime_can_be_played(
await hass.async_block_till_done() await hass.async_block_till_done()
downstairs_chime_mock = mock_ring_devices.get_device(123456) 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") state = hass.states.get("siren.downstairs_siren")
assert state.state == "unknown" assert state.state == "unknown"
@ -117,7 +117,7 @@ async def test_motion_chime_can_be_played(
await hass.async_block_till_done() await hass.async_block_till_done()
downstairs_chime_mock = mock_ring_devices.get_device(123456) 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") state = hass.states.get("siren.downstairs_siren")
assert state.state == "unknown" 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})) assert not any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
downstairs_chime_mock = mock_ring_devices.get_device(123456) 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): with pytest.raises(HomeAssistantError):
await hass.services.async_call( await hass.services.async_call(
@ -155,7 +155,8 @@ async def test_siren_errors_when_turned_on(
{"entity_id": "siren.downstairs_siren", "tone": "motion"}, {"entity_id": "siren.downstairs_siren", "tone": "motion"},
blocking=True, 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 ( assert (
any( any(
flow flow

View File

@ -1,7 +1,5 @@
"""The tests for the Ring switch platform.""" """The tests for the Ring switch platform."""
from unittest.mock import PropertyMock
import pytest import pytest
import ring_doorbell 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})) assert not any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
front_siren_mock = mock_ring_devices.get_device(765432) front_siren_mock = mock_ring_devices.get_device(765432)
p = PropertyMock(side_effect=exception_type) front_siren_mock.async_set_siren.side_effect = exception_type
type(front_siren_mock).siren = p
with pytest.raises(HomeAssistantError): with pytest.raises(HomeAssistantError):
await hass.services.async_call( await hass.services.async_call(
"switch", "turn_on", {"entity_id": "switch.front_siren"}, blocking=True "switch", "turn_on", {"entity_id": "switch.front_siren"}, blocking=True
) )
await hass.async_block_till_done() await hass.async_block_till_done()
p.assert_called_once() front_siren_mock.async_set_siren.assert_called_once()
assert ( assert (
any( any(
flow flow