From 9869b489c46599126c10557f2f3a1049b5146ede Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 15 Mar 2025 16:08:26 +0100 Subject: [PATCH 1/6] Add firmware exception translations --- homeassistant/components/reolink/strings.json | 6 ++++ homeassistant/components/reolink/update.py | 5 ++- homeassistant/components/reolink/util.py | 36 ++++++++++++------- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 335ed92d32e5e..c1dc5dab5a520 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -101,6 +101,12 @@ }, "config_entry_not_ready": { "message": "Error while trying to setup {host}: {err}" + }, + "update_already_running": { + "message": "Reolink firmware update already running, wait on completion before starting another" + }, + "firmware_rate_limit": { + "message": "Reolink firmware update server reached hourly rate limit: updating can be tried again in 1 hour" } }, "issues": { diff --git a/homeassistant/components/reolink/update.py b/homeassistant/components/reolink/update.py index 0744d66fb5bd6..a7c883003b731 100644 --- a/homeassistant/components/reolink/update.py +++ b/homeassistant/components/reolink/update.py @@ -31,7 +31,7 @@ ReolinkHostCoordinatorEntity, ReolinkHostEntityDescription, ) -from .util import ReolinkConfigEntry, ReolinkData +from .util import ReolinkConfigEntry, ReolinkData, raise_translated_error PARALLEL_UPDATES = 0 RESUME_AFTER_INSTALL = 15 @@ -184,6 +184,7 @@ async def async_release_notes(self) -> str | None: f"## Release notes\n\n{new_firmware.release_notes}" ) + @raise_translated_error async def async_install( self, version: str | None, backup: bool, **kwargs: Any ) -> None: @@ -196,6 +197,8 @@ async def async_install( try: await self._host.api.update_firmware(self._channel) except ReolinkError as err: + if err.translation_key: + raise raise HomeAssistantError( translation_domain=DOMAIN, translation_key="firmware_install_error", diff --git a/homeassistant/components/reolink/util.py b/homeassistant/components/reolink/util.py index a5556b66a33f0..e4a87586f64ac 100644 --- a/homeassistant/components/reolink/util.py +++ b/homeassistant/components/reolink/util.py @@ -108,75 +108,87 @@ async def decorator_raise_translated_error(*args: P.args, **kwargs: P.kwargs) -> try: return await func(*args, **kwargs) except InvalidParameterError as err: + key = err.translation_key if err.translation_key else "invalid_parameter" raise ServiceValidationError( translation_domain=DOMAIN, - translation_key="invalid_parameter", + translation_key=key, translation_placeholders={"err": str(err)}, ) from err except ApiError as err: + key = err.translation_key if err.translation_key else "api_error" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key="api_error", + translation_key=key, translation_placeholders={"err": str(err)}, ) from err except InvalidContentTypeError as err: + key = err.translation_key if err.translation_key else "invalid_content_type" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key="invalid_content_type", + translation_key=key, translation_placeholders={"err": str(err)}, ) from err except CredentialsInvalidError as err: + key = err.translation_key if err.translation_key else "invalid_credentials" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key="invalid_credentials", + translation_key=key, translation_placeholders={"err": str(err)}, ) from err except LoginError as err: + key = err.translation_key if err.translation_key else "login_error" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key="login_error", + translation_key=key, translation_placeholders={"err": str(err)}, ) from err except NoDataError as err: + key = err.translation_key if err.translation_key else "no_data" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key="no_data", + translation_key=key, translation_placeholders={"err": str(err)}, ) from err except UnexpectedDataError as err: + key = err.translation_key if err.translation_key else "unexpected_data" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key="unexpected_data", + translation_key=key, translation_placeholders={"err": str(err)}, ) from err except NotSupportedError as err: + key = err.translation_key if err.translation_key else "not_supported" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key="not_supported", + translation_key=key, translation_placeholders={"err": str(err)}, ) from err except SubscriptionError as err: + key = err.translation_key if err.translation_key else "subscription_error" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key="subscription_error", + translation_key=key, translation_placeholders={"err": str(err)}, ) from err except ReolinkConnectionError as err: + key = err.translation_key if err.translation_key else "connection_error" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key="connection_error", + translation_key=key, translation_placeholders={"err": str(err)}, ) from err except ReolinkTimeoutError as err: + key = err.translation_key if err.translation_key else "timeout" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key="timeout", + translation_key=key, translation_placeholders={"err": str(err)}, ) from err except ReolinkError as err: + key = err.translation_key if err.translation_key else "unexpected" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key="unexpected", + translation_key=key, translation_placeholders={"err": str(err)}, ) from err From c45eb93497909272d6cd7c99fef5f95d48dec1b8 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 15 Mar 2025 16:18:10 +0100 Subject: [PATCH 2/6] Add test --- tests/components/reolink/test_update.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/components/reolink/test_update.py b/tests/components/reolink/test_update.py index a6cfe862963f3..d48362516b8e8 100644 --- a/tests/components/reolink/test_update.py +++ b/tests/components/reolink/test_update.py @@ -6,7 +6,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest -from reolink_aio.exceptions import ReolinkError +from reolink_aio.exceptions import ApiError, ReolinkError from reolink_aio.software_version import NewSoftwareVersion from homeassistant.components.reolink.update import POLL_AFTER_INSTALL, POLL_PROGRESS @@ -144,6 +144,17 @@ async def test_update_firm( blocking=True, ) + reolink_connect.update_firmware.side_effect = ApiError( + "Test error", translation_key="firmware_rate_limit" + ) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + # test _async_update_future reolink_connect.camera_sw_version.return_value = "v3.3.0.226_23031644" reolink_connect.firmware_update_available.return_value = False From cd93af4694200b18ae8789326d2fcd23375a784c Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 17 Mar 2025 22:08:25 +0100 Subject: [PATCH 3/6] Much nicer syntax --- homeassistant/components/reolink/util.py | 36 ++++++++---------------- 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/reolink/util.py b/homeassistant/components/reolink/util.py index e4a87586f64ac..e5ac6e106ec64 100644 --- a/homeassistant/components/reolink/util.py +++ b/homeassistant/components/reolink/util.py @@ -108,87 +108,75 @@ async def decorator_raise_translated_error(*args: P.args, **kwargs: P.kwargs) -> try: return await func(*args, **kwargs) except InvalidParameterError as err: - key = err.translation_key if err.translation_key else "invalid_parameter" raise ServiceValidationError( translation_domain=DOMAIN, - translation_key=key, + translation_key=err.translation_key or "invalid_parameter", translation_placeholders={"err": str(err)}, ) from err except ApiError as err: - key = err.translation_key if err.translation_key else "api_error" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=key, + translation_key=err.translation_key or "api_error", translation_placeholders={"err": str(err)}, ) from err except InvalidContentTypeError as err: - key = err.translation_key if err.translation_key else "invalid_content_type" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=key, + translation_key=err.translation_key or "invalid_content_type", translation_placeholders={"err": str(err)}, ) from err except CredentialsInvalidError as err: - key = err.translation_key if err.translation_key else "invalid_credentials" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=key, + translation_key=err.translation_key or "invalid_credentials", translation_placeholders={"err": str(err)}, ) from err except LoginError as err: - key = err.translation_key if err.translation_key else "login_error" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=key, + translation_key=err.translation_key or "login_error", translation_placeholders={"err": str(err)}, ) from err except NoDataError as err: - key = err.translation_key if err.translation_key else "no_data" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=key, + translation_key=err.translation_key or "no_data", translation_placeholders={"err": str(err)}, ) from err except UnexpectedDataError as err: - key = err.translation_key if err.translation_key else "unexpected_data" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=key, + translation_key=err.translation_key or "unexpected_data", translation_placeholders={"err": str(err)}, ) from err except NotSupportedError as err: - key = err.translation_key if err.translation_key else "not_supported" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=key, + translation_key=err.translation_key or "not_supported", translation_placeholders={"err": str(err)}, ) from err except SubscriptionError as err: - key = err.translation_key if err.translation_key else "subscription_error" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=key, + translation_key=err.translation_key or "subscription_error", translation_placeholders={"err": str(err)}, ) from err except ReolinkConnectionError as err: - key = err.translation_key if err.translation_key else "connection_error" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=key, + translation_key=err.translation_key or "connection_error", translation_placeholders={"err": str(err)}, ) from err except ReolinkTimeoutError as err: - key = err.translation_key if err.translation_key else "timeout" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=key, + translation_key=err.translation_key or "timeout", translation_placeholders={"err": str(err)}, ) from err except ReolinkError as err: - key = err.translation_key if err.translation_key else "unexpected" raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=key, + translation_key=err.translation_key or "unexpected", translation_placeholders={"err": str(err)}, ) from err From 86bf1881c8abf04cf1280b57a4c97aea8cec0696 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 21 Mar 2025 09:11:32 +0100 Subject: [PATCH 4/6] Check if translation key is present in string.json --- homeassistant/components/reolink/util.py | 35 ++++++++++++++++-------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/reolink/util.py b/homeassistant/components/reolink/util.py index e5ac6e106ec64..cfd4abe5bc06f 100644 --- a/homeassistant/components/reolink/util.py +++ b/homeassistant/components/reolink/util.py @@ -28,6 +28,7 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers.storage import Store from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.translation import async_get_exception_message from .const import DOMAIN @@ -97,6 +98,16 @@ def get_device_uid_and_ch( return (device_uid, ch, is_chime) +def check_translation_key(err: ReolinkError) -> str | None: + """Check if the translation key from the upstream library is present.""" + if not err.translation_key: + return None + if async_get_exception_message(DOMAIN, err.translation_key) == err.translation_key: + # translation key not found in strings.json + return None + return err.translation_key + + # Decorators def raise_translated_error[**P, R]( func: Callable[P, Awaitable[R]], @@ -110,73 +121,73 @@ async def decorator_raise_translated_error(*args: P.args, **kwargs: P.kwargs) -> except InvalidParameterError as err: raise ServiceValidationError( translation_domain=DOMAIN, - translation_key=err.translation_key or "invalid_parameter", + translation_key=check_translation_key(err) or "invalid_parameter", translation_placeholders={"err": str(err)}, ) from err except ApiError as err: raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=err.translation_key or "api_error", + translation_key=check_translation_key(err) or "api_error", translation_placeholders={"err": str(err)}, ) from err except InvalidContentTypeError as err: raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=err.translation_key or "invalid_content_type", + translation_key=check_translation_key(err) or "invalid_content_type", translation_placeholders={"err": str(err)}, ) from err except CredentialsInvalidError as err: raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=err.translation_key or "invalid_credentials", + translation_key=check_translation_key(err) or "invalid_credentials", translation_placeholders={"err": str(err)}, ) from err except LoginError as err: raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=err.translation_key or "login_error", + translation_key=check_translation_key(err) or "login_error", translation_placeholders={"err": str(err)}, ) from err except NoDataError as err: raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=err.translation_key or "no_data", + translation_key=check_translation_key(err) or "no_data", translation_placeholders={"err": str(err)}, ) from err except UnexpectedDataError as err: raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=err.translation_key or "unexpected_data", + translation_key=check_translation_key(err) or "unexpected_data", translation_placeholders={"err": str(err)}, ) from err except NotSupportedError as err: raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=err.translation_key or "not_supported", + translation_key=check_translation_key(err) or "not_supported", translation_placeholders={"err": str(err)}, ) from err except SubscriptionError as err: raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=err.translation_key or "subscription_error", + translation_key=check_translation_key(err) or "subscription_error", translation_placeholders={"err": str(err)}, ) from err except ReolinkConnectionError as err: raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=err.translation_key or "connection_error", + translation_key=check_translation_key(err) or "connection_error", translation_placeholders={"err": str(err)}, ) from err except ReolinkTimeoutError as err: raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=err.translation_key or "timeout", + translation_key=check_translation_key(err) or "timeout", translation_placeholders={"err": str(err)}, ) from err except ReolinkError as err: raise HomeAssistantError( translation_domain=DOMAIN, - translation_key=err.translation_key or "unexpected", + translation_key=check_translation_key(err) or "unexpected", translation_placeholders={"err": str(err)}, ) from err From 98a54622d3102e64dd4069352f718b6f7cee6eb9 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 21 Mar 2025 09:44:38 +0100 Subject: [PATCH 5/6] fix tests --- homeassistant/components/reolink/util.py | 2 +- tests/components/reolink/test_util.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/reolink/util.py b/homeassistant/components/reolink/util.py index cfd4abe5bc06f..241c370709d39 100644 --- a/homeassistant/components/reolink/util.py +++ b/homeassistant/components/reolink/util.py @@ -27,8 +27,8 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.storage import Store -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.translation import async_get_exception_message +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN diff --git a/tests/components/reolink/test_util.py b/tests/components/reolink/test_util.py index f66f4682b980b..9183fa394bca3 100644 --- a/tests/components/reolink/test_util.py +++ b/tests/components/reolink/test_util.py @@ -40,6 +40,14 @@ ApiError("Test error"), HomeAssistantError, ), + ( + ApiError("Test error", translation_key="firmware_rate_limit"), + HomeAssistantError, + ), + ( + ApiError("Test error", translation_key="not_in_json.strings"), + HomeAssistantError, + ), ( CredentialsInvalidError("Test error"), HomeAssistantError, From 930020e4b19052e47965a750a29dc07ea6bd54be Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 21 Mar 2025 09:47:35 +0100 Subject: [PATCH 6/6] fix typo --- tests/components/reolink/test_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/reolink/test_util.py b/tests/components/reolink/test_util.py index 9183fa394bca3..73db25eb7dc60 100644 --- a/tests/components/reolink/test_util.py +++ b/tests/components/reolink/test_util.py @@ -45,7 +45,7 @@ HomeAssistantError, ), ( - ApiError("Test error", translation_key="not_in_json.strings"), + ApiError("Test error", translation_key="not_in_strings.json"), HomeAssistantError, ), (