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
This commit is contained in:
Maciej Bieniek 2025-03-24 17:16:29 +01:00 committed by GitHub
parent 5f093180ab
commit 95cc3e31f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 222 additions and 54 deletions

View File

@ -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()

View File

@ -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:

View File

@ -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()

View File

@ -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()

View File

@ -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(

View File

@ -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()

View File

@ -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()

View File

@ -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": {

View File

@ -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:

View File

@ -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",
),
],
)

View File

@ -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,

View File

@ -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(

View File

@ -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,

View File

@ -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,

View File

@ -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")