mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 17:27:10 +00:00
Add exception translations to ring integration (#136468)
* Add exception translations to ring integration * Do not include exception details in exception translations * Don't check last_update_success for auth errors and update tests * Do not log errors twice * Update post review
This commit is contained in:
parent
5629b995ce
commit
2f5816c5b6
@ -31,6 +31,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import RingConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RingDataCoordinator
|
||||
from .entity import RingDeviceT, RingEntity, exception_wrap
|
||||
|
||||
@ -218,8 +219,13 @@ class RingCam(RingEntity[RingDoorBell], Camera):
|
||||
) -> None:
|
||||
"""Handle a WebRTC candidate."""
|
||||
if candidate.sdp_m_line_index is None:
|
||||
msg = "The sdp_m_line_index is required for ring webrtc streaming"
|
||||
raise HomeAssistantError(msg)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="sdp_m_line_index_required",
|
||||
translation_placeholders={
|
||||
"device": self._device.name,
|
||||
},
|
||||
)
|
||||
await self._device.on_webrtc_candidate(
|
||||
session_id, candidate.candidate, candidate.sdp_m_line_index
|
||||
)
|
||||
|
@ -27,7 +27,7 @@ from homeassistant.helpers.update_coordinator import (
|
||||
UpdateFailed,
|
||||
)
|
||||
|
||||
from .const import SCAN_INTERVAL
|
||||
from .const import DOMAIN, SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -45,26 +45,6 @@ class RingData:
|
||||
type RingConfigEntry = ConfigEntry[RingData]
|
||||
|
||||
|
||||
async def _call_api[*_Ts, _R](
|
||||
hass: HomeAssistant,
|
||||
target: Callable[[*_Ts], Coroutine[Any, Any, _R]],
|
||||
*args: *_Ts,
|
||||
msg_suffix: str = "",
|
||||
) -> _R:
|
||||
try:
|
||||
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)
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except RingTimeout as err:
|
||||
raise UpdateFailed(
|
||||
f"Timeout communicating with API{msg_suffix}: {err}"
|
||||
) from err
|
||||
except RingError as err:
|
||||
raise UpdateFailed(f"Error communicating with API{msg_suffix}: {err}") from err
|
||||
|
||||
|
||||
class RingDataCoordinator(DataUpdateCoordinator[RingDevices]):
|
||||
"""Base class for device coordinators."""
|
||||
|
||||
@ -87,12 +67,37 @@ class RingDataCoordinator(DataUpdateCoordinator[RingDevices]):
|
||||
self.ring_api: Ring = ring_api
|
||||
self.first_call: bool = True
|
||||
|
||||
async def _call_api[*_Ts, _R](
|
||||
self,
|
||||
target: Callable[[*_Ts], Coroutine[Any, Any, _R]],
|
||||
*args: *_Ts,
|
||||
) -> _R:
|
||||
try:
|
||||
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)
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_authentication",
|
||||
) from err
|
||||
except RingTimeout as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_timeout",
|
||||
) from err
|
||||
except RingError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_error",
|
||||
) from err
|
||||
|
||||
async def _async_update_data(self) -> RingDevices:
|
||||
"""Fetch data from API endpoint."""
|
||||
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 self._call_api(getattr(self.ring_api, update_method))
|
||||
self.first_call = False
|
||||
devices: RingDevices = self.ring_api.devices()
|
||||
subscribed_device_ids = set(self.async_contexts())
|
||||
@ -104,18 +109,14 @@ class RingDataCoordinator(DataUpdateCoordinator[RingDevices]):
|
||||
async with TaskGroup() as tg:
|
||||
if device.has_capability("history"):
|
||||
tg.create_task(
|
||||
_call_api(
|
||||
self.hass,
|
||||
self._call_api(
|
||||
lambda device: device.async_history(limit=10),
|
||||
device,
|
||||
msg_suffix=f" for device {device.name}", # device_id is the mac
|
||||
)
|
||||
)
|
||||
tg.create_task(
|
||||
_call_api(
|
||||
self.hass,
|
||||
self._call_api(
|
||||
device.async_update_health_data,
|
||||
msg_suffix=f" for device {device.name}",
|
||||
)
|
||||
)
|
||||
except ExceptionGroup as eg:
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, Concatenate, Generic, TypeVar, cast
|
||||
|
||||
from ring_doorbell import (
|
||||
@ -36,6 +37,8 @@ _RingCoordinatorT = TypeVar(
|
||||
bound=(RingDataCoordinator | RingListenCoordinator),
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DeprecatedInfo:
|
||||
@ -62,14 +65,22 @@ def exception_wrap[_RingBaseEntityT: RingBaseEntity[Any, Any], **_P, _R](
|
||||
return await async_func(self, *args, **kwargs)
|
||||
except AuthenticationError as err:
|
||||
self.coordinator.config_entry.async_start_reauth(self.hass)
|
||||
raise HomeAssistantError(err) from err
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_authentication",
|
||||
) from err
|
||||
except RingTimeout as err:
|
||||
raise HomeAssistantError(
|
||||
f"Timeout communicating with API {async_func}: {err}"
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_timeout",
|
||||
) from err
|
||||
except RingError as err:
|
||||
_LOGGER.debug(
|
||||
"Error calling %s in platform %s: ", async_func.__name__, self.platform
|
||||
)
|
||||
raise HomeAssistantError(
|
||||
f"Error communicating with API{async_func}: {err}"
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_error",
|
||||
) from err
|
||||
|
||||
return _wrap
|
||||
|
@ -141,6 +141,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"api_authentication": {
|
||||
"message": "Authentication error communicating with Ring API"
|
||||
},
|
||||
"api_timeout": {
|
||||
"message": "Timeout communicating with Ring API"
|
||||
},
|
||||
"api_error": {
|
||||
"message": "Error communicating with Ring API"
|
||||
},
|
||||
"sdp_m_line_index_required": {
|
||||
"message": "Error negotiating stream for {device}"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_entity": {
|
||||
"title": "Detected deprecated {platform} entity usage",
|
||||
|
@ -436,9 +436,9 @@ async def test_camera_webrtc(
|
||||
assert response
|
||||
assert response.get("success") is False
|
||||
assert response["error"]["code"] == "home_assistant_error"
|
||||
msg = "The sdp_m_line_index is required for ring webrtc streaming"
|
||||
assert msg in response["error"].get("message")
|
||||
assert msg in caplog.text
|
||||
error_msg = f"Error negotiating stream for {front_camera_mock.name}"
|
||||
assert error_msg in response["error"].get("message")
|
||||
assert error_msg in caplog.text
|
||||
front_camera_mock.on_webrtc_candidate.assert_called_once()
|
||||
|
||||
# Answer message
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.components.ring.const import (
|
||||
CONF_LISTEN_CREDENTIALS,
|
||||
SCAN_INTERVAL,
|
||||
)
|
||||
from homeassistant.components.ring.coordinator import RingEventListener
|
||||
from homeassistant.components.ring.coordinator import RingConfigEntry, RingEventListener
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_TOKEN, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
@ -80,12 +80,12 @@ async def test_auth_failed_on_setup(
|
||||
("error_type", "log_msg"),
|
||||
[
|
||||
(
|
||||
RingTimeout,
|
||||
"Timeout communicating with API: ",
|
||||
RingTimeout("Some internal error info"),
|
||||
"Timeout communicating with Ring API",
|
||||
),
|
||||
(
|
||||
RingError,
|
||||
"Error communicating with API: ",
|
||||
RingError("Some internal error info"),
|
||||
"Error communicating with Ring API",
|
||||
),
|
||||
],
|
||||
ids=["timeout-error", "other-error"],
|
||||
@ -95,6 +95,7 @@ async def test_error_on_setup(
|
||||
mock_ring_client,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
error_type,
|
||||
log_msg,
|
||||
) -> None:
|
||||
@ -166,11 +167,11 @@ async def test_auth_failure_on_device_update(
|
||||
[
|
||||
(
|
||||
RingTimeout,
|
||||
"Error fetching devices data: Timeout communicating with API: ",
|
||||
"Error fetching devices data: Timeout communicating with Ring API",
|
||||
),
|
||||
(
|
||||
RingError,
|
||||
"Error fetching devices data: Error communicating with API: ",
|
||||
"Error fetching devices data: Error communicating with Ring API",
|
||||
),
|
||||
],
|
||||
ids=["timeout-error", "other-error"],
|
||||
@ -178,7 +179,7 @@ async def test_auth_failure_on_device_update(
|
||||
async def test_error_on_global_update(
|
||||
hass: HomeAssistant,
|
||||
mock_ring_client,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_config_entry: RingConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
error_type,
|
||||
@ -189,15 +190,35 @@ 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.async_update_devices.side_effect = error_type
|
||||
coordinator = mock_config_entry.runtime_data.devices_coordinator
|
||||
assert coordinator
|
||||
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
with patch.object(
|
||||
coordinator, "_async_update_data", wraps=coordinator._async_update_data
|
||||
) as refresh_spy:
|
||||
error = error_type("Some internal error info 1")
|
||||
mock_ring_client.async_update_devices.side_effect = error
|
||||
|
||||
assert log_msg in caplog.text
|
||||
freezer.tick(SCAN_INTERVAL * 2)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert hass.config_entries.async_get_entry(mock_config_entry.entry_id)
|
||||
refresh_spy.assert_called()
|
||||
assert coordinator.last_exception.__cause__ == error
|
||||
assert log_msg in caplog.text
|
||||
|
||||
# Check log is not being spammed.
|
||||
refresh_spy.reset_mock()
|
||||
error2 = error_type("Some internal error info 2")
|
||||
caplog.clear()
|
||||
mock_ring_client.async_update_devices.side_effect = error2
|
||||
freezer.tick(SCAN_INTERVAL * 2)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
refresh_spy.assert_called()
|
||||
assert coordinator.last_exception.__cause__ == error2
|
||||
assert log_msg not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -205,11 +226,11 @@ async def test_error_on_global_update(
|
||||
[
|
||||
(
|
||||
RingTimeout,
|
||||
"Error fetching devices data: Timeout communicating with API for device Front: ",
|
||||
"Error fetching devices data: Timeout communicating with Ring API",
|
||||
),
|
||||
(
|
||||
RingError,
|
||||
"Error fetching devices data: Error communicating with API for device Front: ",
|
||||
"Error fetching devices data: Error communicating with Ring API",
|
||||
),
|
||||
],
|
||||
ids=["timeout-error", "other-error"],
|
||||
@ -218,7 +239,7 @@ async def test_error_on_device_update(
|
||||
hass: HomeAssistant,
|
||||
mock_ring_client,
|
||||
mock_ring_devices,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_config_entry: RingConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
error_type,
|
||||
@ -229,15 +250,36 @@ async def test_error_on_device_update(
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
front_door_doorbell = mock_ring_devices.get_device(765432)
|
||||
front_door_doorbell.async_history.side_effect = error_type
|
||||
coordinator = mock_config_entry.runtime_data.devices_coordinator
|
||||
assert coordinator
|
||||
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
with patch.object(
|
||||
coordinator, "_async_update_data", wraps=coordinator._async_update_data
|
||||
) as refresh_spy:
|
||||
error = error_type("Some internal error info 1")
|
||||
front_door_doorbell = mock_ring_devices.get_device(765432)
|
||||
front_door_doorbell.async_history.side_effect = error
|
||||
|
||||
assert log_msg in caplog.text
|
||||
assert hass.config_entries.async_get_entry(mock_config_entry.entry_id)
|
||||
freezer.tick(SCAN_INTERVAL * 2)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
refresh_spy.assert_called()
|
||||
assert coordinator.last_exception.__cause__ == error
|
||||
assert log_msg in caplog.text
|
||||
|
||||
# Check log is not being spammed.
|
||||
error2 = error_type("Some internal error info 2")
|
||||
front_door_doorbell.async_history.side_effect = error2
|
||||
refresh_spy.reset_mock()
|
||||
caplog.clear()
|
||||
freezer.tick(SCAN_INTERVAL * 2)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
refresh_spy.assert_called()
|
||||
assert coordinator.last_exception.__cause__ == error2
|
||||
assert log_msg not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
Loading…
x
Reference in New Issue
Block a user