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