From 40ed0562bc7e3df76c4edfe76efd8a32dfd80eb5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 23 Jan 2025 13:48:46 +0100 Subject: [PATCH] Add translated action exceptions to Airgradient (#136322) * Add translated action exceptions to Airgradient * Add translated action exceptions to Airgradient --- .../components/airgradient/button.py | 3 +- .../components/airgradient/coordinator.py | 6 ++- .../components/airgradient/entity.py | 34 +++++++++++++++- .../components/airgradient/number.py | 3 +- .../components/airgradient/quality_scale.yaml | 4 +- .../components/airgradient/select.py | 3 +- .../components/airgradient/strings.json | 11 ++++++ .../components/airgradient/switch.py | 4 +- tests/components/airgradient/test_button.py | 38 +++++++++++++++++- tests/components/airgradient/test_number.py | 38 +++++++++++++++++- tests/components/airgradient/test_select.py | 39 ++++++++++++++++++- tests/components/airgradient/test_switch.py | 37 +++++++++++++++++- 12 files changed, 208 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/airgradient/button.py b/homeassistant/components/airgradient/button.py index c4cbb92f9ba..ea7b12062e8 100644 --- a/homeassistant/components/airgradient/button.py +++ b/homeassistant/components/airgradient/button.py @@ -18,7 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AirGradientConfigEntry from .const import DOMAIN from .coordinator import AirGradientCoordinator -from .entity import AirGradientEntity +from .entity import AirGradientEntity, exception_handler PARALLEL_UPDATES = 1 @@ -102,6 +102,7 @@ class AirGradientButton(AirGradientEntity, ButtonEntity): self.entity_description = description self._attr_unique_id = f"{coordinator.serial_number}-{description.key}" + @exception_handler async def async_press(self) -> None: """Press the button.""" await self.entity_description.press_fn(self.coordinator.client) diff --git a/homeassistant/components/airgradient/coordinator.py b/homeassistant/components/airgradient/coordinator.py index 03d58645853..d2fc2a9de1b 100644 --- a/homeassistant/components/airgradient/coordinator.py +++ b/homeassistant/components/airgradient/coordinator.py @@ -55,7 +55,11 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]): measures = await self.client.get_current_measures() config = await self.client.get_config() except AirGradientError as error: - raise UpdateFailed(error) from error + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="update_error", + translation_placeholders={"error": str(error)}, + ) from error if measures.firmware_version != self._current_version: device_registry = dr.async_get(self.hass) device_entry = device_registry.async_get_device( diff --git a/homeassistant/components/airgradient/entity.py b/homeassistant/components/airgradient/entity.py index 588a799610b..51256051259 100644 --- a/homeassistant/components/airgradient/entity.py +++ b/homeassistant/components/airgradient/entity.py @@ -1,7 +1,11 @@ """Base class for AirGradient entities.""" -from airgradient import get_model_name +from collections.abc import Callable, Coroutine +from typing import Any, Concatenate +from airgradient import AirGradientConnectionError, AirGradientError, get_model_name + +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -26,3 +30,31 @@ class AirGradientEntity(CoordinatorEntity[AirGradientCoordinator]): serial_number=coordinator.serial_number, sw_version=measures.firmware_version, ) + + +def exception_handler[_EntityT: AirGradientEntity, **_P]( + func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]], +) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]: + """Decorate AirGradient calls to handle exceptions. + + A decorator that wraps the passed in function, catches AirGradient errors. + """ + + async def handler(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None: + try: + await func(self, *args, **kwargs) + except AirGradientConnectionError as error: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="communication_error", + translation_placeholders={"error": str(error)}, + ) from error + + except AirGradientError as error: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="unknown_error", + translation_placeholders={"error": str(error)}, + ) from error + + return handler diff --git a/homeassistant/components/airgradient/number.py b/homeassistant/components/airgradient/number.py index 95a26f66530..4265215fa25 100644 --- a/homeassistant/components/airgradient/number.py +++ b/homeassistant/components/airgradient/number.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AirGradientConfigEntry from .const import DOMAIN from .coordinator import AirGradientCoordinator -from .entity import AirGradientEntity +from .entity import AirGradientEntity, exception_handler PARALLEL_UPDATES = 1 @@ -123,6 +123,7 @@ class AirGradientNumber(AirGradientEntity, NumberEntity): """Return the state of the number.""" return self.entity_description.value_fn(self.coordinator.data.config) + @exception_handler async def async_set_native_value(self, value: float) -> None: """Set the selected value.""" await self.entity_description.set_value_fn(self.coordinator.client, int(value)) diff --git a/homeassistant/components/airgradient/quality_scale.yaml b/homeassistant/components/airgradient/quality_scale.yaml index 333c64ded00..a8904e71af5 100644 --- a/homeassistant/components/airgradient/quality_scale.yaml +++ b/homeassistant/components/airgradient/quality_scale.yaml @@ -29,7 +29,7 @@ rules: unique-config-entry: done # Silver - action-exceptions: todo + action-exceptions: done config-entry-unloading: done docs-configuration-parameters: status: exempt @@ -68,7 +68,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: todo + exception-translations: done icon-translations: done reconfiguration-flow: todo repair-issues: diff --git a/homeassistant/components/airgradient/select.py b/homeassistant/components/airgradient/select.py index 467904654a4..8c15102ad3a 100644 --- a/homeassistant/components/airgradient/select.py +++ b/homeassistant/components/airgradient/select.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AirGradientConfigEntry from .const import DOMAIN, PM_STANDARD, PM_STANDARD_REVERSE from .coordinator import AirGradientCoordinator -from .entity import AirGradientEntity +from .entity import AirGradientEntity, exception_handler PARALLEL_UPDATES = 1 @@ -218,6 +218,7 @@ class AirGradientSelect(AirGradientEntity, SelectEntity): """Return the state of the select.""" return self.entity_description.value_fn(self.coordinator.data.config) + @exception_handler async def async_select_option(self, option: str) -> None: """Change the selected option.""" await self.entity_description.set_value_fn(self.coordinator.client, option) diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index f3f78ea8fc9..f3b0bbdd60c 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -165,5 +165,16 @@ "name": "Post data to Airgradient" } } + }, + "exceptions": { + "communication_error": { + "message": "An error occurred while communicating with the Airgradient device: {error}" + }, + "unknown_error": { + "message": "An unknown error occurred while communicating with the Airgradient device: {error}" + }, + "update_error": { + "message": "An error occurred while communicating with the Airgradient device: {error}" + } } } diff --git a/homeassistant/components/airgradient/switch.py b/homeassistant/components/airgradient/switch.py index 6cdcbb53fae..55835fa30a6 100644 --- a/homeassistant/components/airgradient/switch.py +++ b/homeassistant/components/airgradient/switch.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AirGradientConfigEntry from .const import DOMAIN from .coordinator import AirGradientCoordinator -from .entity import AirGradientEntity +from .entity import AirGradientEntity, exception_handler PARALLEL_UPDATES = 1 @@ -101,11 +101,13 @@ class AirGradientSwitch(AirGradientEntity, SwitchEntity): """Return the state of the switch.""" return self.entity_description.value_fn(self.coordinator.data.config) + @exception_handler async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self.entity_description.set_value_fn(self.coordinator.client, True) await self.coordinator.async_request_refresh() + @exception_handler async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self.entity_description.set_value_fn(self.coordinator.client, False) diff --git a/tests/components/airgradient/test_button.py b/tests/components/airgradient/test_button.py index 83de2c2f048..2440669b6e8 100644 --- a/tests/components/airgradient/test_button.py +++ b/tests/components/airgradient/test_button.py @@ -3,14 +3,16 @@ from datetime import timedelta from unittest.mock import AsyncMock, patch -from airgradient import Config +from airgradient import AirGradientConnectionError, AirGradientError, Config from freezegun.api import FrozenDateTimeFactory +import pytest from syrupy import SnapshotAssertion from homeassistant.components.airgradient.const import DOMAIN from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from . import setup_integration @@ -97,3 +99,37 @@ async def test_cloud_creates_no_button( await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 + + +@pytest.mark.parametrize( + ("exception", "error_message"), + [ + ( + AirGradientConnectionError("Something happened"), + "An error occurred while communicating with the Airgradient device: Something happened", + ), + ( + AirGradientError("Something else happened"), + "An unknown error occurred while communicating with the Airgradient device: Something else happened", + ), + ], +) +async def test_exception_handling( + hass: HomeAssistant, + mock_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, + exception: Exception, + error_message: str, +) -> None: + """Test exception handling.""" + await setup_integration(hass, mock_config_entry) + mock_airgradient_client.request_co2_calibration.side_effect = exception + with pytest.raises(HomeAssistantError, match=error_message): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: "button.airgradient_calibrate_co2_sensor", + }, + blocking=True, + ) diff --git a/tests/components/airgradient/test_number.py b/tests/components/airgradient/test_number.py index 7aabda8f81c..2cbd72d033a 100644 --- a/tests/components/airgradient/test_number.py +++ b/tests/components/airgradient/test_number.py @@ -3,8 +3,9 @@ from datetime import timedelta from unittest.mock import AsyncMock, patch -from airgradient import Config +from airgradient import AirGradientConnectionError, AirGradientError, Config from freezegun.api import FrozenDateTimeFactory +import pytest from syrupy import SnapshotAssertion from homeassistant.components.airgradient.const import DOMAIN @@ -15,6 +16,7 @@ from homeassistant.components.number import ( ) from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from . import setup_integration @@ -99,3 +101,37 @@ async def test_cloud_creates_no_number( await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 + + +@pytest.mark.parametrize( + ("exception", "error_message"), + [ + ( + AirGradientConnectionError("Something happened"), + "An error occurred while communicating with the Airgradient device: Something happened", + ), + ( + AirGradientError("Something else happened"), + "An unknown error occurred while communicating with the Airgradient device: Something else happened", + ), + ], +) +async def test_exception_handling( + hass: HomeAssistant, + mock_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, + exception: Exception, + error_message: str, +) -> None: + """Test exception handling.""" + await setup_integration(hass, mock_config_entry) + + mock_airgradient_client.set_display_brightness.side_effect = exception + with pytest.raises(HomeAssistantError, match=error_message): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + service_data={ATTR_VALUE: 50}, + target={ATTR_ENTITY_ID: "number.airgradient_display_brightness"}, + blocking=True, + ) diff --git a/tests/components/airgradient/test_select.py b/tests/components/airgradient/test_select.py index de4a7beaaa7..b8ae2cefa4e 100644 --- a/tests/components/airgradient/test_select.py +++ b/tests/components/airgradient/test_select.py @@ -3,7 +3,7 @@ from datetime import timedelta from unittest.mock import AsyncMock, patch -from airgradient import Config +from airgradient import AirGradientConnectionError, AirGradientError, Config from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion @@ -15,6 +15,7 @@ from homeassistant.components.select import ( ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from . import setup_integration @@ -94,3 +95,39 @@ async def test_cloud_creates_no_number( await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 + + +@pytest.mark.parametrize( + ("exception", "error_message"), + [ + ( + AirGradientConnectionError("Something happened"), + "An error occurred while communicating with the Airgradient device: Something happened", + ), + ( + AirGradientError("Something else happened"), + "An unknown error occurred while communicating with the Airgradient device: Something else happened", + ), + ], +) +async def test_exception_handling( + hass: HomeAssistant, + mock_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, + exception: Exception, + error_message: str, +) -> None: + """Test exception handling.""" + await setup_integration(hass, mock_config_entry) + + mock_airgradient_client.set_configuration_control.side_effect = exception + with pytest.raises(HomeAssistantError, match=error_message): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.airgradient_configuration_source", + ATTR_OPTION: "local", + }, + blocking=True, + ) diff --git a/tests/components/airgradient/test_switch.py b/tests/components/airgradient/test_switch.py index a0cbdd17d75..475f38f554c 100644 --- a/tests/components/airgradient/test_switch.py +++ b/tests/components/airgradient/test_switch.py @@ -3,8 +3,9 @@ from datetime import timedelta from unittest.mock import AsyncMock, patch -from airgradient import Config +from airgradient import AirGradientConnectionError, AirGradientError, Config from freezegun.api import FrozenDateTimeFactory +import pytest from syrupy import SnapshotAssertion from homeassistant.components.airgradient.const import DOMAIN @@ -16,6 +17,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from . import setup_integration @@ -99,3 +101,36 @@ async def test_cloud_creates_no_switch( await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 + + +@pytest.mark.parametrize( + ("exception", "error_message"), + [ + ( + AirGradientConnectionError("Something happened"), + "An error occurred while communicating with the Airgradient device: Something happened", + ), + ( + AirGradientError("Something else happened"), + "An unknown error occurred while communicating with the Airgradient device: Something else happened", + ), + ], +) +async def test_exception_handling( + hass: HomeAssistant, + mock_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, + exception: Exception, + error_message: str, +) -> None: + """Test exception handling.""" + await setup_integration(hass, mock_config_entry) + + mock_airgradient_client.enable_sharing_data.side_effect = exception + with pytest.raises(HomeAssistantError, match=error_message): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + target={ATTR_ENTITY_ID: "switch.airgradient_post_data_to_airgradient"}, + blocking=True, + )