Improve error handling and add exception translations for Nettigo Air Monitor integration (#141183)

* Add update_error

* Add device_communication_error

* Add auth_error

* Add device_communication_action_error

* Coverage
This commit is contained in:
Maciej Bieniek 2025-03-23 20:23:11 +01:00 committed by GitHub
parent ef84fc52af
commit 1f122ea54d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 120 additions and 10 deletions

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import logging
from aiohttp.client_exceptions import ClientConnectorError, ClientError
from aiohttp.client_exceptions import ClientError
from nettigo_air_monitor import (
ApiError,
AuthFailedError,
@ -38,15 +38,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: NAMConfigEntry) -> bool:
options = ConnectionOptions(host=host, username=username, password=password)
try:
nam = await NettigoAirMonitor.create(websession, options)
except (ApiError, ClientError, ClientConnectorError, TimeoutError) as err:
raise ConfigEntryNotReady from err
except (ApiError, ClientError) as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="device_communication_error",
translation_placeholders={"device": entry.title},
) from err
try:
await nam.async_check_credentials()
except ApiError as err:
raise ConfigEntryNotReady from err
except (ApiError, ClientError) as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="device_communication_error",
translation_placeholders={"device": entry.title},
) from err
except AuthFailedError as err:
raise ConfigEntryAuthFailed from err
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="auth_error",
translation_placeholders={"device": entry.title},
) from err
coordinator = NAMDataUpdateCoordinator(hass, entry, nam)
await coordinator.async_config_entry_first_refresh()

View File

@ -4,6 +4,9 @@ from __future__ import annotations
import logging
from aiohttp.client_exceptions import ClientError
from nettigo_air_monitor import ApiError, AuthFailedError
from homeassistant.components.button import (
ButtonDeviceClass,
ButtonEntity,
@ -11,9 +14,11 @@ from homeassistant.components.button import (
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import NAMConfigEntry, NAMDataUpdateCoordinator
PARALLEL_UPDATES = 1
@ -59,4 +64,16 @@ class NAMButton(CoordinatorEntity[NAMDataUpdateCoordinator], ButtonEntity):
async def async_press(self) -> None:
"""Triggers the restart."""
await self.coordinator.nam.async_restart()
try:
await self.coordinator.nam.async_restart()
except (ApiError, ClientError) as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="device_communication_action_error",
translation_placeholders={
"entity": self.entity_id,
"device": self.coordinator.config_entry.title,
},
) from err
except AuthFailedError:
self.coordinator.config_entry.async_start_reauth(self.hass)

View File

@ -64,6 +64,10 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator[NAMSensors]):
# We do not need to catch AuthFailed exception here because sensor data is
# always available without authorization.
except (ApiError, InvalidSensorDataError, RetryError) as error:
raise UpdateFailed(error) from error
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_error",
translation_placeholders={"device": self.config_entry.title},
) from error
return data

View File

@ -205,5 +205,19 @@
"name": "Last restart"
}
}
},
"exceptions": {
"auth_error": {
"message": "Authentication failed for {device}, please update your credentials"
},
"device_communication_error": {
"message": "An error occurred while communicating with {device}"
},
"device_communication_action_error": {
"message": "An error occurred while calling action for {entity} for {device}"
},
"update_error": {
"message": "An error occurred while retrieving data from {device}"
}
}
}

View File

@ -2,9 +2,20 @@
from unittest.mock import patch
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, ButtonDeviceClass
from aiohttp.client_exceptions import ClientError
from nettigo_air_monitor import ApiError, AuthFailedError
import pytest
from homeassistant.components.button import (
DOMAIN as BUTTON_DOMAIN,
SERVICE_PRESS,
ButtonDeviceClass,
)
from homeassistant.components.nam import DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
@ -38,7 +49,7 @@ async def test_button_press(hass: HomeAssistant) -> None:
):
await hass.services.async_call(
BUTTON_DOMAIN,
"press",
SERVICE_PRESS,
{ATTR_ENTITY_ID: "button.nettigo_air_monitor_restart"},
blocking=True,
)
@ -49,3 +60,55 @@ async def test_button_press(hass: HomeAssistant) -> None:
state = hass.states.get("button.nettigo_air_monitor_restart")
assert state
assert state.state == now.isoformat()
@pytest.mark.parametrize(("exc"), [ApiError("API Error"), ClientError])
async def test_button_press_exc(hass: HomeAssistant, exc: Exception) -> None:
"""Test button press when exception occurs."""
await init_integration(hass)
with (
patch(
"homeassistant.components.nam.NettigoAirMonitor.async_restart",
side_effect=exc,
),
pytest.raises(
HomeAssistantError,
match="An error occurred while calling action for button.nettigo_air_monitor_restart",
),
):
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: "button.nettigo_air_monitor_restart"},
blocking=True,
)
async def test_button_press_auth_error(hass: HomeAssistant) -> None:
"""Test button press when auth error occurs."""
entry = await init_integration(hass)
with patch(
"homeassistant.components.nam.NettigoAirMonitor.async_restart",
side_effect=AuthFailedError("auth error"),
):
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: "button.nettigo_air_monitor_restart"},
blocking=True,
)
assert entry.state is ConfigEntryState.LOADED
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
flow = flows[0]
assert flow.get("step_id") == "reauth_confirm"
assert flow.get("handler") == DOMAIN
assert "context" in flow
assert flow["context"].get("source") == SOURCE_REAUTH
assert flow["context"].get("entry_id") == entry.entry_id