diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index ae0badb3d84..29dfb4ee57b 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -73,7 +73,9 @@ async def async_setup_entry( ) as err: await host.stop() raise ConfigEntryNotReady( - f"Error while trying to setup {host.api.host}:{host.api.port}: {err!s}" + translation_domain=DOMAIN, + translation_key="config_entry_not_ready", + translation_placeholders={"host": host.api.host, "err": str(err)}, ) from err except BaseException: await host.stop() diff --git a/homeassistant/components/reolink/button.py b/homeassistant/components/reolink/button.py index cd1e1b05fae..6b1fcc65a2f 100644 --- a/homeassistant/components/reolink/button.py +++ b/homeassistant/components/reolink/button.py @@ -7,7 +7,6 @@ from dataclasses import dataclass from typing import Any from reolink_aio.api import GuardEnum, Host, PtzEnum -from reolink_aio.exceptions import ReolinkError import voluptuous as vol from homeassistant.components.button import ( @@ -18,7 +17,6 @@ from homeassistant.components.button import ( from homeassistant.components.camera import CameraEntityFeature from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, @@ -31,7 +29,7 @@ from .entity import ( ReolinkHostCoordinatorEntity, ReolinkHostEntityDescription, ) -from .util import ReolinkConfigEntry, ReolinkData +from .util import ReolinkConfigEntry, ReolinkData, raise_translated_error PARALLEL_UPDATES = 0 ATTR_SPEED = "speed" @@ -205,22 +203,18 @@ class ReolinkButtonEntity(ReolinkChannelCoordinatorEntity, ButtonEntity): ): self._attr_supported_features = SUPPORT_PTZ_SPEED + @raise_translated_error async def async_press(self) -> None: """Execute the button action.""" - try: - await self.entity_description.method(self._host.api, self._channel) - except ReolinkError as err: - raise HomeAssistantError(err) from err + await self.entity_description.method(self._host.api, self._channel) + @raise_translated_error async def async_ptz_move(self, **kwargs: Any) -> None: """PTZ move with speed.""" speed = kwargs[ATTR_SPEED] - try: - await self._host.api.set_ptz_command( - self._channel, command=self.entity_description.ptz_cmd, speed=speed - ) - except ReolinkError as err: - raise HomeAssistantError(err) from err + await self._host.api.set_ptz_command( + self._channel, command=self.entity_description.ptz_cmd, speed=speed + ) class ReolinkHostButtonEntity(ReolinkHostCoordinatorEntity, ButtonEntity): @@ -237,9 +231,7 @@ class ReolinkHostButtonEntity(ReolinkHostCoordinatorEntity, ButtonEntity): self.entity_description = entity_description super().__init__(reolink_data) + @raise_translated_error async def async_press(self) -> None: """Execute the button action.""" - try: - await self.entity_description.method(self._host.api) - except ReolinkError as err: - raise HomeAssistantError(err) from err + await self.entity_description.method(self._host.api) diff --git a/homeassistant/components/reolink/camera.py b/homeassistant/components/reolink/camera.py index 26ef0b0f4fc..d9b3cb67f70 100644 --- a/homeassistant/components/reolink/camera.py +++ b/homeassistant/components/reolink/camera.py @@ -6,7 +6,6 @@ from dataclasses import dataclass import logging from reolink_aio.api import DUAL_LENS_MODELS -from reolink_aio.exceptions import ReolinkError from homeassistant.components.camera import ( Camera, @@ -14,11 +13,10 @@ from homeassistant.components.camera import ( CameraEntityFeature, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import ReolinkChannelCoordinatorEntity, ReolinkChannelEntityDescription -from .util import ReolinkConfigEntry, ReolinkData +from .util import ReolinkConfigEntry, ReolinkData, raise_translated_error _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 @@ -142,13 +140,11 @@ class ReolinkCamera(ReolinkChannelCoordinatorEntity, Camera): self._channel, self.entity_description.stream ) + @raise_translated_error async def async_camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a still image response from the camera.""" - try: - return await self._host.api.get_snapshot( - self._channel, self.entity_description.stream - ) - except ReolinkError as err: - raise HomeAssistantError(err) from err + return await self._host.api.get_snapshot( + self._channel, self.entity_description.stream + ) diff --git a/homeassistant/components/reolink/light.py b/homeassistant/components/reolink/light.py index 3bd9a120798..bbb9592dd76 100644 --- a/homeassistant/components/reolink/light.py +++ b/homeassistant/components/reolink/light.py @@ -7,7 +7,6 @@ from dataclasses import dataclass from typing import Any from reolink_aio.api import Host -from reolink_aio.exceptions import InvalidParameterError, ReolinkError from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -17,7 +16,6 @@ from homeassistant.components.light import ( ) from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import ( @@ -26,7 +24,7 @@ from .entity import ( ReolinkHostCoordinatorEntity, ReolinkHostEntityDescription, ) -from .util import ReolinkConfigEntry, ReolinkData +from .util import ReolinkConfigEntry, ReolinkData, raise_translated_error PARALLEL_UPDATES = 0 @@ -154,37 +152,28 @@ class ReolinkLightEntity(ReolinkChannelCoordinatorEntity, LightEntity): return round(255 * bright_pct / 100.0) + @raise_translated_error async def async_turn_off(self, **kwargs: Any) -> None: """Turn light off.""" - try: - await self.entity_description.turn_on_off_fn( - self._host.api, self._channel, False - ) - except ReolinkError as err: - raise HomeAssistantError(err) from err + await self.entity_description.turn_on_off_fn( + self._host.api, self._channel, False + ) self.async_write_ha_state() + @raise_translated_error async def async_turn_on(self, **kwargs: Any) -> None: """Turn light on.""" if ( brightness := kwargs.get(ATTR_BRIGHTNESS) ) is not None and self.entity_description.set_brightness_fn is not None: brightness_pct = int(brightness / 255.0 * 100) - try: - await self.entity_description.set_brightness_fn( - self._host.api, self._channel, brightness_pct - ) - except InvalidParameterError as err: - raise ServiceValidationError(err) from err - except ReolinkError as err: - raise HomeAssistantError(err) from err - - try: - await self.entity_description.turn_on_off_fn( - self._host.api, self._channel, True + await self.entity_description.set_brightness_fn( + self._host.api, self._channel, brightness_pct ) - except ReolinkError as err: - raise HomeAssistantError(err) from err + + await self.entity_description.turn_on_off_fn( + self._host.api, self._channel, True + ) self.async_write_ha_state() @@ -209,18 +198,14 @@ class ReolinkHostLightEntity(ReolinkHostCoordinatorEntity, LightEntity): """Return true if light is on.""" return self.entity_description.is_on_fn(self._host.api) + @raise_translated_error async def async_turn_off(self, **kwargs: Any) -> None: """Turn light off.""" - try: - await self.entity_description.turn_on_off_fn(self._host.api, False) - except ReolinkError as err: - raise HomeAssistantError(err) from err + await self.entity_description.turn_on_off_fn(self._host.api, False) self.async_write_ha_state() + @raise_translated_error async def async_turn_on(self, **kwargs: Any) -> None: """Turn light on.""" - try: - await self.entity_description.turn_on_off_fn(self._host.api, True) - except ReolinkError as err: - raise HomeAssistantError(err) from err + await self.entity_description.turn_on_off_fn(self._host.api, True) self.async_write_ha_state() diff --git a/homeassistant/components/reolink/number.py b/homeassistant/components/reolink/number.py index 692b43bca9e..e4b52c85d45 100644 --- a/homeassistant/components/reolink/number.py +++ b/homeassistant/components/reolink/number.py @@ -7,7 +7,6 @@ from dataclasses import dataclass from typing import Any from reolink_aio.api import Chime, Host -from reolink_aio.exceptions import InvalidParameterError, ReolinkError from homeassistant.components.number import ( NumberEntity, @@ -16,7 +15,6 @@ from homeassistant.components.number import ( ) from homeassistant.const import EntityCategory, UnitOfTime from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import ( @@ -27,7 +25,7 @@ from .entity import ( ReolinkHostCoordinatorEntity, ReolinkHostEntityDescription, ) -from .util import ReolinkConfigEntry, ReolinkData +from .util import ReolinkConfigEntry, ReolinkData, raise_translated_error PARALLEL_UPDATES = 0 @@ -589,14 +587,10 @@ class ReolinkNumberEntity(ReolinkChannelCoordinatorEntity, NumberEntity): """State of the number entity.""" return self.entity_description.value(self._host.api, self._channel) + @raise_translated_error async def async_set_native_value(self, value: float) -> None: """Update the current value.""" - try: - await self.entity_description.method(self._host.api, self._channel, value) - except InvalidParameterError as err: - raise ServiceValidationError(err) from err - except ReolinkError as err: - raise HomeAssistantError(err) from err + await self.entity_description.method(self._host.api, self._channel, value) self.async_write_ha_state() @@ -621,14 +615,10 @@ class ReolinkHostNumberEntity(ReolinkHostCoordinatorEntity, NumberEntity): """State of the number entity.""" return self.entity_description.value(self._host.api) + @raise_translated_error async def async_set_native_value(self, value: float) -> None: """Update the current value.""" - try: - await self.entity_description.method(self._host.api, value) - except InvalidParameterError as err: - raise ServiceValidationError(err) from err - except ReolinkError as err: - raise HomeAssistantError(err) from err + await self.entity_description.method(self._host.api, value) self.async_write_ha_state() @@ -654,12 +644,8 @@ class ReolinkChimeNumberEntity(ReolinkChimeCoordinatorEntity, NumberEntity): """State of the number entity.""" return self.entity_description.value(self._chime) + @raise_translated_error async def async_set_native_value(self, value: float) -> None: """Update the current value.""" - try: - await self.entity_description.method(self._chime, value) - except InvalidParameterError as err: - raise ServiceValidationError(err) from err - except ReolinkError as err: - raise HomeAssistantError(err) from err + await self.entity_description.method(self._chime, value) self.async_write_ha_state() diff --git a/homeassistant/components/reolink/quality_scale.yaml b/homeassistant/components/reolink/quality_scale.yaml index 540cf19e22a..5cc054b7a4c 100644 --- a/homeassistant/components/reolink/quality_scale.yaml +++ b/homeassistant/components/reolink/quality_scale.yaml @@ -54,7 +54,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: todo + exception-translations: done icon-translations: done reconfiguration-flow: done repair-issues: done diff --git a/homeassistant/components/reolink/select.py b/homeassistant/components/reolink/select.py index 8625f7fb600..7a74be2e28c 100644 --- a/homeassistant/components/reolink/select.py +++ b/homeassistant/components/reolink/select.py @@ -19,12 +19,10 @@ from reolink_aio.api import ( StatusLedEnum, TrackMethodEnum, ) -from reolink_aio.exceptions import InvalidParameterError, ReolinkError from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.const import EntityCategory, UnitOfDataRate, UnitOfFrequency from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import ( @@ -33,7 +31,7 @@ from .entity import ( ReolinkChimeCoordinatorEntity, ReolinkChimeEntityDescription, ) -from .util import ReolinkConfigEntry, ReolinkData +from .util import ReolinkConfigEntry, ReolinkData, raise_translated_error _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 @@ -354,14 +352,10 @@ class ReolinkSelectEntity(ReolinkChannelCoordinatorEntity, SelectEntity): self._log_error = True return option + @raise_translated_error async def async_select_option(self, option: str) -> None: """Change the selected option.""" - try: - await self.entity_description.method(self._host.api, self._channel, option) - except InvalidParameterError as err: - raise ServiceValidationError(err) from err - except ReolinkError as err: - raise HomeAssistantError(err) from err + await self.entity_description.method(self._host.api, self._channel, option) self.async_write_ha_state() @@ -396,12 +390,8 @@ class ReolinkChimeSelectEntity(ReolinkChimeCoordinatorEntity, SelectEntity): self._log_error = True return option + @raise_translated_error async def async_select_option(self, option: str) -> None: """Change the selected option.""" - try: - await self.entity_description.method(self._chime, option) - except InvalidParameterError as err: - raise ServiceValidationError(err) from err - except ReolinkError as err: - raise HomeAssistantError(err) from err + await self.entity_description.method(self._chime, option) self.async_write_ha_state() diff --git a/homeassistant/components/reolink/services.py b/homeassistant/components/reolink/services.py index 326093e7a93..acd31fe0d7d 100644 --- a/homeassistant/components/reolink/services.py +++ b/homeassistant/components/reolink/services.py @@ -4,18 +4,17 @@ from __future__ import annotations from reolink_aio.api import Chime from reolink_aio.enums import ChimeToneEnum -from reolink_aio.exceptions import InvalidParameterError, ReolinkError import voluptuous as vol from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import device_registry as dr from .const import DOMAIN from .host import ReolinkHost -from .util import get_device_uid_and_ch +from .util import get_device_uid_and_ch, raise_translated_error ATTR_RINGTONE = "ringtone" @@ -24,6 +23,7 @@ ATTR_RINGTONE = "ringtone" def async_setup_services(hass: HomeAssistant) -> None: """Set up Reolink services.""" + @raise_translated_error async def async_play_chime(service_call: ServiceCall) -> None: """Play a ringtone.""" service_data = service_call.data @@ -58,12 +58,7 @@ def async_setup_services(hass: HomeAssistant) -> None: ) ringtone = service_data[ATTR_RINGTONE] - try: - await chime.play(ChimeToneEnum[ringtone].value) - except InvalidParameterError as err: - raise ServiceValidationError(err) from err - except ReolinkError as err: - raise HomeAssistantError(err) from err + await chime.play(ChimeToneEnum[ringtone].value) hass.services.async_register( DOMAIN, diff --git a/homeassistant/components/reolink/siren.py b/homeassistant/components/reolink/siren.py index cb12eb5d38c..74bb227d078 100644 --- a/homeassistant/components/reolink/siren.py +++ b/homeassistant/components/reolink/siren.py @@ -5,8 +5,6 @@ from __future__ import annotations from dataclasses import dataclass from typing import Any -from reolink_aio.exceptions import InvalidParameterError, ReolinkError - from homeassistant.components.siren import ( ATTR_DURATION, ATTR_VOLUME_LEVEL, @@ -15,11 +13,10 @@ from homeassistant.components.siren import ( SirenEntityFeature, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import ReolinkChannelCoordinatorEntity, ReolinkChannelEntityDescription -from .util import ReolinkConfigEntry, ReolinkData +from .util import ReolinkConfigEntry, ReolinkData, raise_translated_error PARALLEL_UPDATES = 0 @@ -77,26 +74,15 @@ class ReolinkSirenEntity(ReolinkChannelCoordinatorEntity, SirenEntity): self.entity_description = entity_description super().__init__(reolink_data, channel) + @raise_translated_error async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the siren.""" if (volume := kwargs.get(ATTR_VOLUME_LEVEL)) is not None: - try: - await self._host.api.set_volume(self._channel, int(volume * 100)) - except InvalidParameterError as err: - raise ServiceValidationError(err) from err - except ReolinkError as err: - raise HomeAssistantError(err) from err + await self._host.api.set_volume(self._channel, int(volume * 100)) duration = kwargs.get(ATTR_DURATION) - try: - await self._host.api.set_siren(self._channel, True, duration) - except InvalidParameterError as err: - raise ServiceValidationError(err) from err - except ReolinkError as err: - raise HomeAssistantError(err) from err + await self._host.api.set_siren(self._channel, True, duration) + @raise_translated_error async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the siren.""" - try: - await self._host.api.set_siren(self._channel, False, None) - except ReolinkError as err: - raise HomeAssistantError(err) from err + await self._host.api.set_siren(self._channel, False, None) diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index ac73581ce22..53152131bdb 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -55,6 +55,45 @@ }, "service_not_chime": { "message": "Reolink play_chime error: {device_name} is not a chime" + }, + "invalid_parameter": { + "message": "Invalid input parameter: {err}" + }, + "api_error": { + "message": "The device responded with a error: {err}" + }, + "invalid_content_type": { + "message": "Received a different content type than expected: {err}" + }, + "invalid_credentials": { + "message": "Invalid credentials: {err}" + }, + "login_error": { + "message": "Error during login attempt: {err}" + }, + "no_data": { + "message": "Device returned no data: {err}" + }, + "unexpected_data": { + "message": "Device returned unexpected data: {err}" + }, + "not_supported": { + "message": "Function not supported by this device: {err}" + }, + "subscription_error": { + "message": "Error during ONVIF subscription: {err}" + }, + "connection_error": { + "message": "Could not connect to the device: {err}" + }, + "timeout": { + "message": "Timeout waiting on a response: {err}" + }, + "firmware_install_error": { + "message": "Error trying to update Reolink firmware: {err}" + }, + "config_entry_not_ready": { + "message": "Error while trying to setup {host}: {err}" } }, "issues": { diff --git a/homeassistant/components/reolink/switch.py b/homeassistant/components/reolink/switch.py index c274609599d..b970d04c257 100644 --- a/homeassistant/components/reolink/switch.py +++ b/homeassistant/components/reolink/switch.py @@ -7,12 +7,10 @@ from dataclasses import dataclass from typing import Any from reolink_aio.api import Chime, Host -from reolink_aio.exceptions import ReolinkError from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -25,7 +23,7 @@ from .entity import ( ReolinkHostCoordinatorEntity, ReolinkHostEntityDescription, ) -from .util import ReolinkConfigEntry, ReolinkData +from .util import ReolinkConfigEntry, ReolinkData, raise_translated_error PARALLEL_UPDATES = 0 @@ -430,20 +428,16 @@ class ReolinkSwitchEntity(ReolinkChannelCoordinatorEntity, SwitchEntity): """Return true if switch is on.""" return self.entity_description.value(self._host.api, self._channel) + @raise_translated_error async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - try: - await self.entity_description.method(self._host.api, self._channel, True) - except ReolinkError as err: - raise HomeAssistantError(err) from err + await self.entity_description.method(self._host.api, self._channel, True) self.async_write_ha_state() + @raise_translated_error async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - try: - await self.entity_description.method(self._host.api, self._channel, False) - except ReolinkError as err: - raise HomeAssistantError(err) from err + await self.entity_description.method(self._host.api, self._channel, False) self.async_write_ha_state() @@ -466,20 +460,16 @@ class ReolinkNVRSwitchEntity(ReolinkHostCoordinatorEntity, SwitchEntity): """Return true if switch is on.""" return self.entity_description.value(self._host.api) + @raise_translated_error async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - try: - await self.entity_description.method(self._host.api, True) - except ReolinkError as err: - raise HomeAssistantError(err) from err + await self.entity_description.method(self._host.api, True) self.async_write_ha_state() + @raise_translated_error async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - try: - await self.entity_description.method(self._host.api, False) - except ReolinkError as err: - raise HomeAssistantError(err) from err + await self.entity_description.method(self._host.api, False) self.async_write_ha_state() @@ -503,18 +493,14 @@ class ReolinkChimeSwitchEntity(ReolinkChimeCoordinatorEntity, SwitchEntity): """Return true if switch is on.""" return self.entity_description.value(self._chime) + @raise_translated_error async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - try: - await self.entity_description.method(self._chime, True) - except ReolinkError as err: - raise HomeAssistantError(err) from err + await self.entity_description.method(self._chime, True) self.async_write_ha_state() + @raise_translated_error async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - try: - await self.entity_description.method(self._chime, False) - except ReolinkError as err: - raise HomeAssistantError(err) from err + await self.entity_description.method(self._chime, False) self.async_write_ha_state() diff --git a/homeassistant/components/reolink/update.py b/homeassistant/components/reolink/update.py index aa607e2b29e..5a8c7d7dc08 100644 --- a/homeassistant/components/reolink/update.py +++ b/homeassistant/components/reolink/update.py @@ -24,6 +24,7 @@ from homeassistant.helpers.update_coordinator import ( ) from . import DEVICE_UPDATE_INTERVAL +from .const import DOMAIN from .entity import ( ReolinkChannelCoordinatorEntity, ReolinkChannelEntityDescription, @@ -196,7 +197,9 @@ class ReolinkUpdateBaseEntity( await self._host.api.update_firmware(self._channel) except ReolinkError as err: raise HomeAssistantError( - f"Error trying to update Reolink firmware: {err}" + translation_domain=DOMAIN, + translation_key="firmware_install_error", + translation_placeholders={"err": str(err)}, ) from err finally: self.async_write_ha_state() diff --git a/homeassistant/components/reolink/util.py b/homeassistant/components/reolink/util.py index 98c0e7b925b..1a6eab3f61d 100644 --- a/homeassistant/components/reolink/util.py +++ b/homeassistant/components/reolink/util.py @@ -2,10 +2,28 @@ from __future__ import annotations +from collections.abc import Awaitable, Callable, Coroutine from dataclasses import dataclass +from typing import Any, ParamSpec, TypeVar + +from reolink_aio.exceptions import ( + ApiError, + CredentialsInvalidError, + InvalidContentTypeError, + InvalidParameterError, + LoginError, + NoDataError, + NotSupportedError, + ReolinkConnectionError, + ReolinkError, + ReolinkTimeoutError, + SubscriptionError, + UnexpectedDataError, +) from homeassistant import config_entries from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -53,3 +71,89 @@ def get_device_uid_and_ch( else: ch = host.api.channel_for_uid(device_uid[1]) return (device_uid, ch, is_chime) + + +T = TypeVar("T") +P = ParamSpec("P") + + +# Decorators +def raise_translated_error( + func: Callable[P, Awaitable[T]], +) -> Callable[P, Coroutine[Any, Any, T]]: + """Wrap a reolink-aio function to translate any potential errors.""" + + async def decorator_raise_translated_error(*args: P.args, **kwargs: P.kwargs) -> T: + """Try a reolink-aio function and translate any potential errors.""" + try: + return await func(*args, **kwargs) + except InvalidParameterError as err: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_parameter", + translation_placeholders={"err": str(err)}, + ) from err + except ApiError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="api_error", + translation_placeholders={"err": str(err)}, + ) from err + except InvalidContentTypeError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="invalid_content_type", + translation_placeholders={"err": str(err)}, + ) from err + except CredentialsInvalidError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="invalid_credentials", + translation_placeholders={"err": str(err)}, + ) from err + except LoginError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="login_error", + translation_placeholders={"err": str(err)}, + ) from err + except NoDataError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="no_data", + translation_placeholders={"err": str(err)}, + ) from err + except UnexpectedDataError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="unexpected_data", + translation_placeholders={"err": str(err)}, + ) from err + except NotSupportedError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="not_supported", + translation_placeholders={"err": str(err)}, + ) from err + except SubscriptionError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="subscription_error", + translation_placeholders={"err": str(err)}, + ) from err + except ReolinkConnectionError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="connection_error", + translation_placeholders={"err": str(err)}, + ) from err + except ReolinkTimeoutError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="timeout", + translation_placeholders={"err": str(err)}, + ) from err + except ReolinkError as err: + raise HomeAssistantError(err) from err + + return decorator_raise_translated_error diff --git a/tests/components/reolink/test_util.py b/tests/components/reolink/test_util.py new file mode 100644 index 00000000000..f66f4682b98 --- /dev/null +++ b/tests/components/reolink/test_util.py @@ -0,0 +1,115 @@ +"""Test the Reolink util functions.""" + +from unittest.mock import MagicMock, patch + +import pytest +from reolink_aio.exceptions import ( + ApiError, + CredentialsInvalidError, + InvalidContentTypeError, + InvalidParameterError, + LoginError, + NoDataError, + NotSupportedError, + ReolinkConnectionError, + ReolinkError, + ReolinkTimeoutError, + SubscriptionError, + UnexpectedDataError, +) + +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError + +from .conftest import TEST_NVR_NAME + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + ("side_effect", "expected"), + [ + ( + ApiError("Test error"), + HomeAssistantError, + ), + ( + CredentialsInvalidError("Test error"), + HomeAssistantError, + ), + ( + InvalidContentTypeError("Test error"), + HomeAssistantError, + ), + ( + InvalidParameterError("Test error"), + ServiceValidationError, + ), + ( + LoginError("Test error"), + HomeAssistantError, + ), + ( + NoDataError("Test error"), + HomeAssistantError, + ), + ( + NotSupportedError("Test error"), + HomeAssistantError, + ), + ( + ReolinkConnectionError("Test error"), + HomeAssistantError, + ), + ( + ReolinkError("Test error"), + HomeAssistantError, + ), + ( + ReolinkTimeoutError("Test error"), + HomeAssistantError, + ), + ( + SubscriptionError("Test error"), + HomeAssistantError, + ), + ( + UnexpectedDataError("Test error"), + HomeAssistantError, + ), + ], +) +async def test_try_function( + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + side_effect: ReolinkError, + expected: Exception, +) -> None: + """Test try_function error translations using number entity.""" + reolink_connect.volume.return_value = 80 + + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.NUMBER]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + entity_id = f"{Platform.NUMBER}.{TEST_NVR_NAME}_volume" + + reolink_connect.set_volume.side_effect = side_effect + with pytest.raises(expected): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 50}, + blocking=True, + ) + + reolink_connect.set_volume.reset_mock(side_effect=True)