From a78ecb3895f9bdb189abeccb3a8b2b33f7c4f17a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Dec 2023 10:53:31 +0100 Subject: [PATCH] Add error handling to Tailwind service methods (#106463) --- homeassistant/components/tailwind/button.py | 12 ++- homeassistant/components/tailwind/cover.py | 66 +++++++++++-- homeassistant/components/tailwind/number.py | 12 ++- .../components/tailwind/strings.json | 11 +++ tests/components/tailwind/test_button.py | 17 ++++ tests/components/tailwind/test_cover.py | 96 ++++++++++++++++++- tests/components/tailwind/test_number.py | 20 ++++ 7 files changed, 219 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/tailwind/button.py b/homeassistant/components/tailwind/button.py index dd9548d131c..019b803901c 100644 --- a/homeassistant/components/tailwind/button.py +++ b/homeassistant/components/tailwind/button.py @@ -5,7 +5,7 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass from typing import Any -from gotailwind import Tailwind +from gotailwind import Tailwind, TailwindError from homeassistant.components.button import ( ButtonDeviceClass, @@ -15,6 +15,7 @@ from homeassistant.components.button import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -62,4 +63,11 @@ class TailwindButtonEntity(TailwindEntity, ButtonEntity): async def async_press(self) -> None: """Trigger button press on the Tailwind device.""" - await self.entity_description.press_fn(self.coordinator.tailwind) + try: + await self.entity_description.press_fn(self.coordinator.tailwind) + except TailwindError as exc: + raise HomeAssistantError( + str(exc), + translation_domain=DOMAIN, + translation_key="communication_error", + ) from exc diff --git a/homeassistant/components/tailwind/cover.py b/homeassistant/components/tailwind/cover.py index 4280b6c4baf..935fa01eee0 100644 --- a/homeassistant/components/tailwind/cover.py +++ b/homeassistant/components/tailwind/cover.py @@ -3,7 +3,13 @@ from __future__ import annotations from typing import Any -from gotailwind import TailwindDoorOperationCommand, TailwindDoorState +from gotailwind import ( + TailwindDoorDisabledError, + TailwindDoorLockedOutError, + TailwindDoorOperationCommand, + TailwindDoorState, + TailwindError, +) from homeassistant.components.cover import ( CoverDeviceClass, @@ -12,6 +18,7 @@ from homeassistant.components.cover import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -56,11 +63,31 @@ class TailwindDoorCoverEntity(TailwindDoorEntity, CoverEntity): """ self._attr_is_opening = True self.async_write_ha_state() - await self.coordinator.tailwind.operate( - door=self.coordinator.data.doors[self.door_id], - operation=TailwindDoorOperationCommand.OPEN, - ) - self._attr_is_opening = False + try: + await self.coordinator.tailwind.operate( + door=self.coordinator.data.doors[self.door_id], + operation=TailwindDoorOperationCommand.OPEN, + ) + except TailwindDoorDisabledError as exc: + raise HomeAssistantError( + str(exc), + translation_domain=DOMAIN, + translation_key="door_disabled", + ) from exc + except TailwindDoorLockedOutError as exc: + raise HomeAssistantError( + str(exc), + translation_domain=DOMAIN, + translation_key="door_locked_out", + ) from exc + except TailwindError as exc: + raise HomeAssistantError( + str(exc), + translation_domain=DOMAIN, + translation_key="communication_error", + ) from exc + finally: + self._attr_is_opening = False await self.coordinator.async_request_refresh() async def async_close_cover(self, **kwargs: Any) -> None: @@ -71,9 +98,28 @@ class TailwindDoorCoverEntity(TailwindDoorEntity, CoverEntity): """ self._attr_is_closing = True self.async_write_ha_state() - await self.coordinator.tailwind.operate( - door=self.coordinator.data.doors[self.door_id], - operation=TailwindDoorOperationCommand.CLOSE, - ) + try: + await self.coordinator.tailwind.operate( + door=self.coordinator.data.doors[self.door_id], + operation=TailwindDoorOperationCommand.CLOSE, + ) + except TailwindDoorDisabledError as exc: + raise HomeAssistantError( + str(exc), + translation_domain=DOMAIN, + translation_key="door_disabled", + ) from exc + except TailwindDoorLockedOutError as exc: + raise HomeAssistantError( + str(exc), + translation_domain=DOMAIN, + translation_key="door_locked_out", + ) from exc + except TailwindError as exc: + raise HomeAssistantError( + str(exc), + translation_domain=DOMAIN, + translation_key="communication_error", + ) from exc self._attr_is_closing = False await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/tailwind/number.py b/homeassistant/components/tailwind/number.py index 88940d110fa..5853e5c2d30 100644 --- a/homeassistant/components/tailwind/number.py +++ b/homeassistant/components/tailwind/number.py @@ -5,12 +5,13 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass from typing import Any -from gotailwind import Tailwind, TailwindDeviceStatus +from gotailwind import Tailwind, TailwindDeviceStatus, TailwindError from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, EntityCategory from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -72,5 +73,12 @@ class TailwindNumberEntity(TailwindEntity, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Change to new number value.""" - await self.entity_description.set_value_fn(self.coordinator.tailwind, value) + try: + await self.entity_description.set_value_fn(self.coordinator.tailwind, value) + except TailwindError as exc: + raise HomeAssistantError( + str(exc), + translation_domain=DOMAIN, + translation_key="communication_error", + ) from exc await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/tailwind/strings.json b/homeassistant/components/tailwind/strings.json index ab472a46739..7ff7fd439cc 100644 --- a/homeassistant/components/tailwind/strings.json +++ b/homeassistant/components/tailwind/strings.json @@ -60,5 +60,16 @@ "name": "Status LED brightness" } } + }, + "exceptions": { + "communication_error": { + "message": "An error occurred while communicating with the Tailwind device." + }, + "door_disabled": { + "message": "The door is disabled and cannot be operated." + }, + "door_locked_out": { + "message": "The door is locked out and cannot be operated." + } } } diff --git a/tests/components/tailwind/test_button.py b/tests/components/tailwind/test_button.py index 708816d733c..a0128d5f498 100644 --- a/tests/components/tailwind/test_button.py +++ b/tests/components/tailwind/test_button.py @@ -1,12 +1,15 @@ """Tests for button entities provided by the Tailwind integration.""" from unittest.mock import MagicMock +from gotailwind import TailwindError import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.tailwind.const import DOMAIN from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er pytestmark = [ @@ -46,3 +49,17 @@ async def test_number_entities( assert (state := hass.states.get(state.entity_id)) assert state.state == "2023-12-17T15:25:00+00:00" + + # Test error handling + mock_tailwind.identify.side_effect = TailwindError("Some error") + + with pytest.raises(HomeAssistantError, match="Some error") as excinfo: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: state.entity_id}, + blocking=True, + ) + + assert excinfo.value.translation_domain == DOMAIN + assert excinfo.value.translation_key == "communication_error" diff --git a/tests/components/tailwind/test_cover.py b/tests/components/tailwind/test_cover.py index e13ab534e5b..9620d6149b7 100644 --- a/tests/components/tailwind/test_cover.py +++ b/tests/components/tailwind/test_cover.py @@ -1,7 +1,12 @@ """Tests for cover entities provided by the Tailwind integration.""" from unittest.mock import ANY, MagicMock -from gotailwind import TailwindDoorOperationCommand +from gotailwind import ( + TailwindDoorDisabledError, + TailwindDoorLockedOutError, + TailwindDoorOperationCommand, + TailwindError, +) import pytest from syrupy.assertion import SnapshotAssertion @@ -10,8 +15,10 @@ from homeassistant.components.cover import ( SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, ) +from homeassistant.components.tailwind.const import DOMAIN from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er pytestmark = pytest.mark.usefixtures("init_integration") @@ -74,3 +81,90 @@ async def test_cover_operations( mock_tailwind.operate.assert_called_with( door=ANY, operation=TailwindDoorOperationCommand.CLOSE ) + + # Test door disabled error handling + mock_tailwind.operate.side_effect = TailwindDoorDisabledError("Door disabled") + + with pytest.raises(HomeAssistantError, match="Door disabled") as excinfo: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + { + ATTR_ENTITY_ID: "cover.door_1", + }, + blocking=True, + ) + + assert excinfo.value.translation_domain == DOMAIN + assert excinfo.value.translation_key == "door_disabled" + + with pytest.raises(HomeAssistantError, match="Door disabled") as excinfo: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + { + ATTR_ENTITY_ID: "cover.door_1", + }, + blocking=True, + ) + + assert excinfo.value.translation_domain == DOMAIN + assert excinfo.value.translation_key == "door_disabled" + + # Test door locked out error handling + mock_tailwind.operate.side_effect = TailwindDoorLockedOutError("Door locked out") + + with pytest.raises(HomeAssistantError, match="Door locked out") as excinfo: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + { + ATTR_ENTITY_ID: "cover.door_1", + }, + blocking=True, + ) + + assert excinfo.value.translation_domain == DOMAIN + assert excinfo.value.translation_key == "door_locked_out" + + with pytest.raises(HomeAssistantError, match="Door locked out") as excinfo: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + { + ATTR_ENTITY_ID: "cover.door_1", + }, + blocking=True, + ) + + assert excinfo.value.translation_domain == DOMAIN + assert excinfo.value.translation_key == "door_locked_out" + + # Test door error handling + mock_tailwind.operate.side_effect = TailwindError("Some error") + + with pytest.raises(HomeAssistantError, match="Some error") as excinfo: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + { + ATTR_ENTITY_ID: "cover.door_1", + }, + blocking=True, + ) + + assert excinfo.value.translation_domain == DOMAIN + assert excinfo.value.translation_key == "communication_error" + + with pytest.raises(HomeAssistantError, match="Some error") as excinfo: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + { + ATTR_ENTITY_ID: "cover.door_1", + }, + blocking=True, + ) + + assert excinfo.value.translation_domain == DOMAIN + assert excinfo.value.translation_key == "communication_error" diff --git a/tests/components/tailwind/test_number.py b/tests/components/tailwind/test_number.py index b67af3d0e62..e16c940b85d 100644 --- a/tests/components/tailwind/test_number.py +++ b/tests/components/tailwind/test_number.py @@ -1,13 +1,16 @@ """Tests for number entities provided by the Tailwind integration.""" from unittest.mock import MagicMock +from gotailwind import TailwindError import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components import number from homeassistant.components.number import ATTR_VALUE, SERVICE_SET_VALUE +from homeassistant.components.tailwind.const import DOMAIN from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er pytestmark = pytest.mark.usefixtures("init_integration") @@ -44,3 +47,20 @@ async def test_number_entities( assert len(mock_tailwind.status_led.mock_calls) == 1 mock_tailwind.status_led.assert_called_with(brightness=42) + + # Test error handling + mock_tailwind.status_led.side_effect = TailwindError("Some error") + + with pytest.raises(HomeAssistantError, match="Some error") as excinfo: + await hass.services.async_call( + number.DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: state.entity_id, + ATTR_VALUE: 42, + }, + blocking=True, + ) + + assert excinfo.value.translation_domain == DOMAIN + assert excinfo.value.translation_key == "communication_error"