Add exception translation to NUT (#141629)

* Add exception translation and test cases

* Capitalize ID in error string

* Test translation placeholders, simplify test cases
This commit is contained in:
tdfountain 2025-03-28 08:43:16 -07:00 committed by GitHub
parent ef06d2c06e
commit 2121b943a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 83 additions and 39 deletions

View File

@ -79,9 +79,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: NutConfigEntry) -> bool:
try: try:
return await data.async_update() return await data.async_update()
except NUTLoginError as err: except NUTLoginError as err:
raise ConfigEntryAuthFailed from err raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="device_authentication",
translation_placeholders={
"err": str(err),
},
) from err
except NUTError as err: except NUTError as err:
raise UpdateFailed(f"Error fetching UPS state: {err}") from err raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="data_fetch_error",
translation_placeholders={
"err": str(err),
},
) from err
coordinator = DataUpdateCoordinator( coordinator = DataUpdateCoordinator(
hass, hass,
@ -328,7 +340,12 @@ class PyNUTData:
await self._client.run_command(self._alias, command_name) await self._client.run_command(self._alias, command_name)
except NUTError as err: except NUTError as err:
raise HomeAssistantError( raise HomeAssistantError(
f"Error running command {command_name}, {err}" translation_domain=DOMAIN,
translation_key="nut_command_error",
translation_placeholders={
"command_name": command_name,
"err": str(err),
},
) from err ) from err
async def async_list_commands(self) -> set[str] | None: async def async_list_commands(self) -> set[str] | None:

View File

@ -51,7 +51,11 @@ async def async_call_action_from_config(
runtime_data = _get_runtime_data_from_device_id(hass, device_id) runtime_data = _get_runtime_data_from_device_id(hass, device_id)
if not runtime_data: if not runtime_data:
raise InvalidDeviceAutomationConfig( raise InvalidDeviceAutomationConfig(
f"Unable to find a NUT device with id {device_id}" translation_domain=DOMAIN,
translation_key="device_invalid",
translation_placeholders={
"device_id": device_id,
},
) )
await runtime_data.data.async_run_command(command_name) await runtime_data.data.async_run_command(command_name)

View File

@ -217,5 +217,19 @@
"switch": { "switch": {
"outlet_number_load_poweronoff": { "name": "Power outlet {outlet_name}" } "outlet_number_load_poweronoff": { "name": "Power outlet {outlet_name}" }
} }
},
"exceptions": {
"data_fetch_error": {
"message": "Error fetching UPS state: {err}"
},
"device_authentication": {
"message": "Device authentication error: {err}"
},
"device_invalid": {
"message": "Unable to find a NUT device with ID {device_id}"
},
"nut_command_error": {
"message": "Error running command {command_name}, {err}"
}
} }
} }

View File

@ -15,6 +15,7 @@ from homeassistant.components.nut import DOMAIN
from homeassistant.components.nut.const import INTEGRATION_SUPPORTED_COMMANDS from homeassistant.components.nut.const import INTEGRATION_SUPPORTED_COMMANDS
from homeassistant.const import CONF_DEVICE_ID, CONF_TYPE from homeassistant.const import CONF_DEVICE_ID, CONF_TYPE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -191,48 +192,39 @@ async def test_action(hass: HomeAssistant, device_registry: dr.DeviceRegistry) -
run_command.assert_called_with("someUps", "beeper.disable") run_command.assert_called_with("someUps", "beeper.disable")
async def test_rund_command_exception( async def test_run_command_exception(
hass: HomeAssistant, hass: HomeAssistant,
device_registry: dr.DeviceRegistry, device_registry: dr.DeviceRegistry,
caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test logged error if run command raises exception.""" """Test if run command raises exception with translation."""
list_commands_return_value = {"beeper.enable": None} command_name = "beeper.enable"
error_message = "Something wrong happened" nut_error_message = "Something wrong happened"
run_command = AsyncMock(side_effect=NUTError(error_message)) run_command = AsyncMock(side_effect=NUTError(nut_error_message))
await async_init_integration( await async_init_integration(
hass, hass,
list_vars={"ups.status": "OL"}, list_vars={"ups.status": "OL"},
list_commands_return_value=list_commands_return_value, list_ups={"ups1": "UPS 1"},
list_commands_return_value={command_name: None},
run_command=run_command, run_command=run_command,
) )
device_entry = next(device for device in device_registry.devices.values()) device_entry = next(device for device in device_registry.devices.values())
assert await async_setup_component( platform = await device_automation.async_get_device_automation_platform(
hass, hass, DOMAIN, DeviceAutomationType.ACTION
automation.DOMAIN,
{
automation.DOMAIN: [
{
"trigger": {
"platform": "event",
"event_type": "test_some_event",
},
"action": {
"domain": DOMAIN,
"device_id": device_entry.id,
"type": "beeper_enable",
},
},
]
},
) )
hass.bus.async_fire("test_some_event") error_message = f"Error running command {command_name}, {nut_error_message}"
await hass.async_block_till_done() with pytest.raises(HomeAssistantError, match=error_message):
await platform.async_call_action_from_config(
assert error_message in caplog.text hass,
{
CONF_TYPE: command_name,
CONF_DEVICE_ID: device_entry.id,
},
{},
None,
)
async def test_action_exception_invalid_device(hass: HomeAssistant) -> None: async def test_action_exception_invalid_device(hass: HomeAssistant) -> None:
@ -248,10 +240,12 @@ async def test_action_exception_invalid_device(hass: HomeAssistant) -> None:
hass, DOMAIN, DeviceAutomationType.ACTION hass, DOMAIN, DeviceAutomationType.ACTION
) )
with pytest.raises(InvalidDeviceAutomationConfig): device_id = "invalid_device_id"
error_message = f"Unable to find a NUT device with ID {device_id}"
with pytest.raises(InvalidDeviceAutomationConfig, match=error_message):
await platform.async_call_action_from_config( await platform.async_call_action_from_config(
hass, hass,
{CONF_TYPE: "beeper.enable", CONF_DEVICE_ID: "invalid_device_id"}, {CONF_TYPE: "beeper.enable", CONF_DEVICE_ID: device_id},
{}, {},
None, None,
) )

View File

@ -4,6 +4,7 @@ from copy import deepcopy
from unittest.mock import patch from unittest.mock import patch
from aionut import NUTError, NUTLoginError from aionut import NUTError, NUTLoginError
import pytest
from homeassistant.components.nut.const import DOMAIN from homeassistant.components.nut.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
@ -56,7 +57,10 @@ async def test_async_setup_entry(hass: HomeAssistant) -> None:
assert not hass.data.get(DOMAIN) assert not hass.data.get(DOMAIN)
async def test_config_not_ready(hass: HomeAssistant) -> None: async def test_config_not_ready(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test for setup failure if connection to broker is missing.""" """Test for setup failure if connection to broker is missing."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
@ -64,6 +68,8 @@ async def test_config_not_ready(hass: HomeAssistant) -> None:
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
nut_error_message = "Something wrong happened"
error_message = f"Error fetching UPS state: {nut_error_message}"
with ( with (
patch( patch(
"homeassistant.components.nut.AIONUTClient.list_ups", "homeassistant.components.nut.AIONUTClient.list_ups",
@ -71,15 +77,20 @@ async def test_config_not_ready(hass: HomeAssistant) -> None:
), ),
patch( patch(
"homeassistant.components.nut.AIONUTClient.list_vars", "homeassistant.components.nut.AIONUTClient.list_vars",
side_effect=NUTError, side_effect=NUTError(nut_error_message),
), ),
): ):
await hass.config_entries.async_setup(entry.entry_id) await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY assert entry.state is ConfigEntryState.SETUP_RETRY
assert error_message in caplog.text
async def test_auth_fails(hass: HomeAssistant) -> None:
async def test_auth_fails(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test for setup failure if auth has changed.""" """Test for setup failure if auth has changed."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
@ -87,6 +98,8 @@ async def test_auth_fails(hass: HomeAssistant) -> None:
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
nut_error_message = "Something wrong happened"
error_message = f"Device authentication error: {nut_error_message}"
with ( with (
patch( patch(
"homeassistant.components.nut.AIONUTClient.list_ups", "homeassistant.components.nut.AIONUTClient.list_ups",
@ -94,13 +107,15 @@ async def test_auth_fails(hass: HomeAssistant) -> None:
), ),
patch( patch(
"homeassistant.components.nut.AIONUTClient.list_vars", "homeassistant.components.nut.AIONUTClient.list_vars",
side_effect=NUTLoginError, side_effect=NUTLoginError(nut_error_message),
), ),
): ):
await hass.config_entries.async_setup(entry.entry_id) await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_ERROR assert entry.state is ConfigEntryState.SETUP_ERROR
assert error_message in caplog.text
flows = hass.config_entries.flow.async_progress() flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1 assert len(flows) == 1
assert flows[0]["context"]["source"] == "reauth" assert flows[0]["context"]["source"] == "reauth"