From 95cc3e31f511f52092f406d893191efa4a8af881 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 24 Mar 2025 17:16:29 +0100 Subject: [PATCH] Add exceptions translations for Shelly integration (#141071) * Add exceptions translations * Improve exception strings for update platform * Fix tests * Improve device_communication_error * Remove error placeholder * Improve tests * Fix test_rpc_set_state_errors * Strings improvement * Remove `device` * Remove `entity` * Fix tests --- homeassistant/components/shelly/__init__.py | 36 ++++++++++++--- homeassistant/components/shelly/button.py | 6 +-- homeassistant/components/shelly/climate.py | 8 +++- .../components/shelly/coordinator.py | 44 ++++++++++++++++--- .../components/shelly/device_trigger.py | 10 ++++- homeassistant/components/shelly/entity.py | 26 ++++++++--- homeassistant/components/shelly/number.py | 10 +++-- homeassistant/components/shelly/strings.json | 37 +++++++++++++++- homeassistant/components/shelly/update.py | 30 +++++++++++-- tests/components/shelly/test_button.py | 8 ++-- tests/components/shelly/test_climate.py | 5 ++- .../components/shelly/test_device_trigger.py | 10 ++++- tests/components/shelly/test_number.py | 5 ++- tests/components/shelly/test_switch.py | 22 ++++++++-- tests/components/shelly/test_update.py | 19 +++++--- 15 files changed, 222 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index a7ee1c029df..8e6417c5d7c 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -189,13 +189,25 @@ async def _async_setup_block_entry( if not device.firmware_supported: async_create_issue_unsupported_firmware(hass, entry) await device.shutdown() - raise ConfigEntryNotReady + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="firmware_unsupported", + translation_placeholders={"device": entry.title}, + ) except (DeviceConnectionError, MacAddressMismatchError) as err: await device.shutdown() - raise ConfigEntryNotReady(repr(err)) from err + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="device_communication_error", + translation_placeholders={"device": entry.title}, + ) from err except InvalidAuthError as err: await device.shutdown() - raise ConfigEntryAuthFailed(repr(err)) from err + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="auth_error", + translation_placeholders={"device": entry.title}, + ) from err runtime_data.block = ShellyBlockCoordinator(hass, entry, device) runtime_data.block.async_setup() @@ -272,16 +284,28 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry) if not device.firmware_supported: async_create_issue_unsupported_firmware(hass, entry) await device.shutdown() - raise ConfigEntryNotReady + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="firmware_unsupported", + translation_placeholders={"device": entry.title}, + ) runtime_data.rpc_script_events = await get_rpc_scripts_event_types( device, ignore_scripts=[BLE_SCRIPT_NAME] ) except (DeviceConnectionError, MacAddressMismatchError, RpcCallError) as err: await device.shutdown() - raise ConfigEntryNotReady(repr(err)) from err + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="device_communication_error", + translation_placeholders={"device": entry.title}, + ) from err except InvalidAuthError as err: await device.shutdown() - raise ConfigEntryAuthFailed(repr(err)) from err + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="auth_error", + translation_placeholders={"device": entry.title}, + ) from err runtime_data.rpc = ShellyRpcCoordinator(hass, entry, device) runtime_data.rpc.async_setup() diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index 15bde4fbdff..06dffba5ead 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -193,8 +193,7 @@ class ShellyBaseButton( translation_key="device_communication_action_error", translation_placeholders={ "entity": self.entity_id, - "device": self.coordinator.device.name, - "error": repr(err), + "device": self.coordinator.name, }, ) from err except RpcCallError as err: @@ -203,8 +202,7 @@ class ShellyBaseButton( translation_key="rpc_call_action_error", translation_placeholders={ "entity": self.entity_id, - "device": self.coordinator.device.name, - "error": repr(err), + "device": self.coordinator.name, }, ) from err except InvalidAuthError: diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index c3612ed3f4f..498f2d3dba9 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -326,8 +326,12 @@ class BlockSleepingClimate( except DeviceConnectionError as err: self.coordinator.last_update_success = False raise HomeAssistantError( - f"Setting state for entity {self.name} failed, state: {kwargs}, error:" - f" {err!r}" + translation_domain=DOMAIN, + translation_key="device_communication_action_error", + translation_placeholders={ + "entity": self.entity_id, + "device": self.coordinator.name, + }, ) from err except InvalidAuthError: await self.coordinator.async_shutdown_device_and_start_reauth() diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 85cf430bc5d..076a6621354 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -378,14 +378,23 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): if self.sleep_period: # Sleeping device, no point polling it, just mark it unavailable raise UpdateFailed( - f"Sleeping device did not update within {self.sleep_period} seconds interval" + translation_domain=DOMAIN, + translation_key="update_error_sleeping_device", + translation_placeholders={ + "device": self.name, + "period": str(self.sleep_period), + }, ) LOGGER.debug("Polling Shelly Block Device - %s", self.name) try: await self.device.update() except DeviceConnectionError as err: - raise UpdateFailed(repr(err)) from err + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="update_error", + translation_placeholders={"device": self.name}, + ) from err except InvalidAuthError: await self.async_shutdown_device_and_start_reauth() @@ -470,7 +479,11 @@ class ShellyRestCoordinator(ShellyCoordinatorBase[BlockDevice]): return await self.device.update_shelly() except (DeviceConnectionError, MacAddressMismatchError) as err: - raise UpdateFailed(repr(err)) from err + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="update_error", + translation_placeholders={"device": self.name}, + ) from err except InvalidAuthError: await self.async_shutdown_device_and_start_reauth() else: @@ -636,7 +649,12 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): if self.sleep_period: # Sleeping device, no point polling it, just mark it unavailable raise UpdateFailed( - f"Sleeping device did not update within {self.sleep_period} seconds interval" + translation_domain=DOMAIN, + translation_key="update_error_sleeping_device", + translation_placeholders={ + "device": self.name, + "period": str(self.sleep_period), + }, ) async with self._connection_lock: @@ -644,7 +662,11 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): return if not await self._async_device_connect_task(): - raise UpdateFailed("Device reconnect error") + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="update_error_reconnect_error", + translation_placeholders={"device": self.name}, + ) async def _async_disconnected(self, reconnect: bool) -> None: """Handle device disconnected.""" @@ -820,13 +842,21 @@ class ShellyRpcPollingCoordinator(ShellyCoordinatorBase[RpcDevice]): async def _async_update_data(self) -> None: """Fetch data.""" if not self.device.connected: - raise UpdateFailed("Device disconnected") + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="update_error_device_disconnected", + translation_placeholders={"device": self.name}, + ) LOGGER.debug("Polling Shelly RPC Device - %s", self.name) try: await self.device.poll() except (DeviceConnectionError, RpcCallError) as err: - raise UpdateFailed(f"Device disconnected: {err!r}") from err + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="update_error", + translation_placeholders={"device": self.name}, + ) from err except InvalidAuthError: await self.async_shutdown_device_and_start_reauth() diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py index 6e96eb5ed21..740e6aae9b2 100644 --- a/homeassistant/components/shelly/device_trigger.py +++ b/homeassistant/components/shelly/device_trigger.py @@ -105,7 +105,9 @@ async def async_validate_trigger_config( return config raise InvalidDeviceAutomationConfig( - f"Invalid ({CONF_TYPE},{CONF_SUBTYPE}): {trigger}" + translation_domain=DOMAIN, + translation_key="invalid_trigger", + translation_placeholders={"trigger": str(trigger)}, ) @@ -137,7 +139,11 @@ async def async_get_triggers( return triggers - raise InvalidDeviceAutomationConfig(f"Device not found: {device_id}") + raise InvalidDeviceAutomationConfig( + translation_domain=DOMAIN, + translation_key="device_not_found", + translation_placeholders={"device": device_id}, + ) async def async_attach_trigger( diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 58ac34fc5ca..9ed3f47b41a 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import CONF_SLEEP_PERIOD, LOGGER +from .const import CONF_SLEEP_PERIOD, DOMAIN, LOGGER from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .utils import ( async_remove_shelly_entity, @@ -345,8 +345,12 @@ class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]): except DeviceConnectionError as err: self.coordinator.last_update_success = False raise HomeAssistantError( - f"Setting state for entity {self.name} failed, state: {kwargs}, error:" - f" {err!r}" + translation_domain=DOMAIN, + translation_key="device_communication_action_error", + translation_placeholders={ + "entity": self.entity_id, + "device": self.coordinator.name, + }, ) from err except InvalidAuthError: await self.coordinator.async_shutdown_device_and_start_reauth() @@ -406,13 +410,21 @@ class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]): except DeviceConnectionError as err: self.coordinator.last_update_success = False raise HomeAssistantError( - f"Call RPC for {self.name} connection error, method: {method}, params:" - f" {params}, error: {err!r}" + translation_domain=DOMAIN, + translation_key="device_communication_action_error", + translation_placeholders={ + "entity": self.entity_id, + "device": self.coordinator.name, + }, ) from err except RpcCallError as err: raise HomeAssistantError( - f"Call RPC for {self.name} request error, method: {method}, params:" - f" {params}, error: {err!r}" + translation_domain=DOMAIN, + translation_key="rpc_call_action_error", + translation_placeholders={ + "entity": self.entity_id, + "device": self.coordinator.name, + }, ) from err except InvalidAuthError: await self.coordinator.async_shutdown_device_and_start_reauth() diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index a8e6de1ca73..c629eb4a57a 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -25,7 +25,7 @@ from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceIn from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_registry import RegistryEntry -from .const import CONF_SLEEP_PERIOD, LOGGER, VIRTUAL_NUMBER_MODE_MAP +from .const import CONF_SLEEP_PERIOD, DOMAIN, LOGGER, VIRTUAL_NUMBER_MODE_MAP from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .entity import ( BlockEntityDescription, @@ -324,8 +324,12 @@ class BlockSleepingNumber(ShellySleepingBlockAttributeEntity, RestoreNumber): except DeviceConnectionError as err: self.coordinator.last_update_success = False raise HomeAssistantError( - f"Setting state for entity {self.name} failed, state: {params}, error:" - f" {err!r}" + translation_domain=DOMAIN, + translation_key="device_communication_action_error", + translation_placeholders={ + "entity": self.entity_id, + "device": self.coordinator.name, + }, ) from err except InvalidAuthError: await self.coordinator.async_shutdown_device_and_start_reauth() diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 22d88928387..8ca16e2a2b5 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -204,11 +204,44 @@ } }, "exceptions": { + "auth_error": { + "message": "Authentication failed for {device}, please update your credentials" + }, + "device_communication_error": { + "message": "Device communication error occurred for {device}" + }, "device_communication_action_error": { - "message": "Device communication error occurred while calling the entity {entity} action for {device} device: {error}" + "message": "Device communication error occurred while calling action for {entity} of {device}" + }, + "device_not_found": { + "message": "{device} not found while configuring device automation triggers" + }, + "firmware_unsupported": { + "message": "{device} is running an unsupported firmware, please update the firmware" + }, + "invalid_trigger": { + "message": "Invalid device automation trigger (type, subtype): {trigger}" + }, + "ota_update_connection_error": { + "message": "Device communication error occurred while triggering OTA update for {device}" + }, + "ota_update_rpc_error": { + "message": "RPC call error occurred while triggering OTA update for {device}" }, "rpc_call_action_error": { - "message": "RPC call error occurred while calling the entity {entity} action for {device} device: {error}" + "message": "RPC call error occurred while calling action for {entity} of {device}" + }, + "update_error": { + "message": "An error occurred while retrieving data from {device}" + }, + "update_error_device_disconnected": { + "message": "An error occurred while retrieving data from {device} because it is disconnected" + }, + "update_error_reconnect_error": { + "message": "An error occurred while reconnecting to {device}" + }, + "update_error_sleeping_device": { + "message": "Sleeping device did not update within {period} seconds interval" } }, "issues": { diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index b1aa84b2640..12ce6dc70cd 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -25,7 +25,14 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from .const import CONF_SLEEP_PERIOD, OTA_BEGIN, OTA_ERROR, OTA_PROGRESS, OTA_SUCCESS +from .const import ( + CONF_SLEEP_PERIOD, + DOMAIN, + OTA_BEGIN, + OTA_ERROR, + OTA_PROGRESS, + OTA_SUCCESS, +) from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .entity import ( RestEntityDescription, @@ -198,7 +205,11 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity): try: result = await self.coordinator.device.trigger_ota_update(beta=beta) except DeviceConnectionError as err: - raise HomeAssistantError(f"Error starting OTA update: {err!r}") from err + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="ota_update_connection_error", + translation_placeholders={"device": self.coordinator.name}, + ) from err except InvalidAuthError: await self.coordinator.async_shutdown_device_and_start_reauth() else: @@ -310,9 +321,20 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): try: await self.coordinator.device.trigger_ota_update(beta=beta) except DeviceConnectionError as err: - raise HomeAssistantError(f"OTA update connection error: {err!r}") from err + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="ota_update_connection_error", + translation_placeholders={"device": self.coordinator.name}, + ) from err except RpcCallError as err: - raise HomeAssistantError(f"OTA update request error: {err!r}") from err + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="ota_update_rpc_error", + translation_placeholders={ + "entity": self.entity_id, + "device": self.coordinator.name, + }, + ) from err except InvalidAuthError: await self.coordinator.async_shutdown_device_and_start_reauth() else: diff --git a/tests/components/shelly/test_button.py b/tests/components/shelly/test_button.py index 2a9720ca7ae..edf11b0e163 100644 --- a/tests/components/shelly/test_button.py +++ b/tests/components/shelly/test_button.py @@ -74,11 +74,11 @@ async def test_rpc_button( [ ( DeviceConnectionError, - "Device communication error occurred while calling the entity button.test_name_reboot action for Test name device", + "Device communication error occurred while calling action for button.test_name_reboot of Test name", ), ( RpcCallError(999), - "RPC call error occurred while calling the entity button.test_name_reboot action for Test name device", + "RPC call error occurred while calling action for button.test_name_reboot of Test name", ), ], ) @@ -212,11 +212,11 @@ async def test_rpc_blu_trv_button( [ ( DeviceConnectionError, - "Device communication error occurred while calling the entity button.trv_name_calibrate action for Test name device", + "Device communication error occurred while calling action for button.trv_name_calibrate of Test name", ), ( RpcCallError(999), - "RPC call error occurred while calling the entity button.trv_name_calibrate action for Test name device", + "RPC call error occurred while calling action for button.trv_name_calibrate of Test name", ), ], ) diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index ac9c7967540..c0bb47bfab6 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -462,7 +462,10 @@ async def test_block_set_mode_connection_error( mock_block_device.mock_online() await hass.async_block_till_done(wait_background_tasks=True) - with pytest.raises(HomeAssistantError): + with pytest.raises( + HomeAssistantError, + match="Device communication error occurred while calling action for climate.test_name of Test name", + ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index 89045208d20..ca9edb19fa7 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -168,7 +168,10 @@ async def test_get_triggers_for_invalid_device_id( connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - with pytest.raises(InvalidDeviceAutomationConfig): + with pytest.raises( + InvalidDeviceAutomationConfig, + match="not found while configuring device automation triggers", + ): await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, invalid_device.id ) @@ -384,7 +387,10 @@ async def test_validate_trigger_invalid_triggers( }, ) - assert "Invalid (type,subtype): ('single', 'button3')" in caplog.text + assert ( + "Invalid device automation trigger (type, subtype): ('single', 'button3')" + in caplog.text + ) async def test_rpc_no_runtime_data( diff --git a/tests/components/shelly/test_number.py b/tests/components/shelly/test_number.py index c032a137bfc..ef5766e0091 100644 --- a/tests/components/shelly/test_number.py +++ b/tests/components/shelly/test_number.py @@ -200,7 +200,10 @@ async def test_block_set_value_connection_error( mock_block_device.mock_online() await hass.async_block_till_done(wait_background_tasks=True) - with pytest.raises(HomeAssistantError): + with pytest.raises( + HomeAssistantError, + match="Device communication error occurred while calling action for number.test_name_valve_position of Test name", + ): await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index 0425f883ad6..fb1c826c67c 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -221,7 +221,10 @@ async def test_block_set_state_connection_error( ) await init_integration(hass, 1) - with pytest.raises(HomeAssistantError): + with pytest.raises( + HomeAssistantError, + match="Device communication error occurred while calling action for switch.test_name_channel_1 of Test name", + ): await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, @@ -360,10 +363,23 @@ async def test_rpc_device_switch_type_lights_mode( assert hass.states.get("switch.test_switch_0") is None -@pytest.mark.parametrize("exc", [DeviceConnectionError, RpcCallError(-1, "error")]) +@pytest.mark.parametrize( + ("exc", "error"), + [ + ( + DeviceConnectionError, + "Device communication error occurred while calling action for switch.test_switch_0 of Test name", + ), + ( + RpcCallError(-1, "error"), + "RPC call error occurred while calling action for switch.test_switch_0 of Test name", + ), + ], +) async def test_rpc_set_state_errors( hass: HomeAssistant, exc: Exception, + error: str, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -373,7 +389,7 @@ async def test_rpc_set_state_errors( monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False) await init_integration(hass, 2) - with pytest.raises(HomeAssistantError): + with pytest.raises(HomeAssistantError, match=error): await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index 9ea66c1acb7..29d72ab4aa8 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -184,14 +184,16 @@ async def test_block_update_connection_error( ) await init_integration(hass, 1) - with pytest.raises(HomeAssistantError) as excinfo: + with pytest.raises( + HomeAssistantError, + match="Device communication error occurred while triggering OTA update for Test name", + ): await hass.services.async_call( UPDATE_DOMAIN, SERVICE_INSTALL, {ATTR_ENTITY_ID: "update.test_name_firmware"}, blocking=True, ) - assert "Error starting OTA update" in str(excinfo.value) @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -673,8 +675,14 @@ async def test_rpc_beta_update( @pytest.mark.parametrize( ("exc", "error"), [ - (DeviceConnectionError, "OTA update connection error: DeviceConnectionError()"), - (RpcCallError(-1, "error"), "OTA update request error"), + ( + DeviceConnectionError, + "Device communication error occurred while triggering OTA update for Test name", + ), + ( + RpcCallError(-1, "error"), + "RPC call error occurred while triggering OTA update for Test name", + ), ], ) @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -701,14 +709,13 @@ async def test_rpc_update_errors( ) await init_integration(hass, 2) - with pytest.raises(HomeAssistantError) as excinfo: + with pytest.raises(HomeAssistantError, match=error): await hass.services.async_call( UPDATE_DOMAIN, SERVICE_INSTALL, {ATTR_ENTITY_ID: "update.test_name_firmware"}, blocking=True, ) - assert error in str(excinfo.value) @pytest.mark.usefixtures("entity_registry_enabled_by_default")