From 7103ea7e8f4a0f9def0731829f18c30cc3d1d5c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joris=20Pelgr=C3=B6m?= Date: Fri, 31 Jan 2025 21:28:23 +0100 Subject: [PATCH] Add exception handling for updating LetPot time entities (#137033) * Handle exceptions for entity edits for LetPot * Set exception-translations: done --- homeassistant/components/letpot/entity.py | 30 +++++++++++ .../components/letpot/quality_scale.yaml | 4 +- homeassistant/components/letpot/strings.json | 8 +++ homeassistant/components/letpot/time.py | 3 +- tests/components/letpot/test_time.py | 52 +++++++++++++++++++ 5 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 tests/components/letpot/test_time.py diff --git a/homeassistant/components/letpot/entity.py b/homeassistant/components/letpot/entity.py index c9a8953b5d5b4..b4d505f409297 100644 --- a/homeassistant/components/letpot/entity.py +++ b/homeassistant/components/letpot/entity.py @@ -1,5 +1,11 @@ """Base class for LetPot entities.""" +from collections.abc import Callable, Coroutine +from typing import Any, Concatenate + +from letpot.exceptions import LetPotConnectionException, LetPotException + +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -23,3 +29,27 @@ def __init__(self, coordinator: LetPotDeviceCoordinator) -> None: model_id=coordinator.device_client.device_model_code, serial_number=coordinator.device.serial_number, ) + + +def exception_handler[_EntityT: LetPotEntity, **_P]( + func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]], +) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]: + """Decorate the function to catch LetPot exceptions and raise them correctly.""" + + async def handler(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None: + try: + await func(self, *args, **kwargs) + except LetPotConnectionException as exception: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="communication_error", + translation_placeholders={"exception": str(exception)}, + ) from exception + except LetPotException as exception: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="unknown_error", + translation_placeholders={"exception": str(exception)}, + ) from exception + + return handler diff --git a/homeassistant/components/letpot/quality_scale.yaml b/homeassistant/components/letpot/quality_scale.yaml index 74b948ffbf741..7f8c3d3c04c02 100644 --- a/homeassistant/components/letpot/quality_scale.yaml +++ b/homeassistant/components/letpot/quality_scale.yaml @@ -29,7 +29,7 @@ rules: unique-config-entry: done # Silver - action-exceptions: todo + action-exceptions: done config-entry-unloading: status: done comment: | @@ -63,7 +63,7 @@ rules: entity-device-class: todo entity-disabled-by-default: todo entity-translations: done - exception-translations: todo + exception-translations: done icon-translations: todo reconfiguration-flow: todo repair-issues: todo diff --git a/homeassistant/components/letpot/strings.json b/homeassistant/components/letpot/strings.json index 93913c2bc4d8a..94d3ad02cfa22 100644 --- a/homeassistant/components/letpot/strings.json +++ b/homeassistant/components/letpot/strings.json @@ -40,5 +40,13 @@ "name": "Light on" } } + }, + "exceptions": { + "communication_error": { + "message": "An error occurred while communicating with the LetPot device: {exception}" + }, + "unknown_error": { + "message": "An unknown error occurred while communicating with the LetPot device: {exception}" + } } } diff --git a/homeassistant/components/letpot/time.py b/homeassistant/components/letpot/time.py index 229f02e0806f3..80ce9743d8c44 100644 --- a/homeassistant/components/letpot/time.py +++ b/homeassistant/components/letpot/time.py @@ -15,7 +15,7 @@ from . import LetPotConfigEntry from .coordinator import LetPotDeviceCoordinator -from .entity import LetPotEntity +from .entity import LetPotEntity, exception_handler # Each change pushes a 'full' device status with the change. The library will cache # pending changes to avoid overwriting, but try to avoid a lot of parallelism. @@ -86,6 +86,7 @@ def native_value(self) -> time | None: """Return the time.""" return self.entity_description.value_fn(self.coordinator.data) + @exception_handler async def async_set_value(self, value: time) -> None: """Set the time.""" await self.entity_description.set_value_fn( diff --git a/tests/components/letpot/test_time.py b/tests/components/letpot/test_time.py new file mode 100644 index 0000000000000..44a03e565c063 --- /dev/null +++ b/tests/components/letpot/test_time.py @@ -0,0 +1,52 @@ +"""Test time entities for the LetPot integration.""" + +from datetime import time +from unittest.mock import MagicMock + +from letpot.exceptions import LetPotConnectionException, LetPotException +import pytest + +from homeassistant.components.time import SERVICE_SET_VALUE +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from . import setup_integration + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + ("exception", "user_error"), + [ + ( + LetPotConnectionException("Connection failed"), + "An error occurred while communicating with the LetPot device: Connection failed", + ), + ( + LetPotException("Random thing failed"), + "An unknown error occurred while communicating with the LetPot device: Random thing failed", + ), + ], +) +async def test_time_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, + mock_device_client: MagicMock, + exception: Exception, + user_error: str, +) -> None: + """Test time entity exception handling.""" + await setup_integration(hass, mock_config_entry) + + mock_device_client.set_light_schedule.side_effect = exception + + assert hass.states.get("time.garden_light_on") is not None + with pytest.raises(HomeAssistantError, match=user_error): + await hass.services.async_call( + "time", + SERVICE_SET_VALUE, + service_data={"time": time(hour=7, minute=0)}, + blocking=True, + target={"entity_id": "time.garden_light_on"}, + )