mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Add error handling to Tailwind service methods (#106463)
This commit is contained in:
parent
68ac4717dc
commit
a78ecb3895
@ -5,7 +5,7 @@ from collections.abc import Awaitable, Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from gotailwind import Tailwind
|
from gotailwind import Tailwind, TailwindError
|
||||||
|
|
||||||
from homeassistant.components.button import (
|
from homeassistant.components.button import (
|
||||||
ButtonDeviceClass,
|
ButtonDeviceClass,
|
||||||
@ -15,6 +15,7 @@ from homeassistant.components.button import (
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
@ -62,4 +63,11 @@ class TailwindButtonEntity(TailwindEntity, ButtonEntity):
|
|||||||
|
|
||||||
async def async_press(self) -> None:
|
async def async_press(self) -> None:
|
||||||
"""Trigger button press on the Tailwind device."""
|
"""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
|
||||||
|
@ -3,7 +3,13 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from gotailwind import TailwindDoorOperationCommand, TailwindDoorState
|
from gotailwind import (
|
||||||
|
TailwindDoorDisabledError,
|
||||||
|
TailwindDoorLockedOutError,
|
||||||
|
TailwindDoorOperationCommand,
|
||||||
|
TailwindDoorState,
|
||||||
|
TailwindError,
|
||||||
|
)
|
||||||
|
|
||||||
from homeassistant.components.cover import (
|
from homeassistant.components.cover import (
|
||||||
CoverDeviceClass,
|
CoverDeviceClass,
|
||||||
@ -12,6 +18,7 @@ from homeassistant.components.cover import (
|
|||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
@ -56,11 +63,31 @@ class TailwindDoorCoverEntity(TailwindDoorEntity, CoverEntity):
|
|||||||
"""
|
"""
|
||||||
self._attr_is_opening = True
|
self._attr_is_opening = True
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
await self.coordinator.tailwind.operate(
|
try:
|
||||||
door=self.coordinator.data.doors[self.door_id],
|
await self.coordinator.tailwind.operate(
|
||||||
operation=TailwindDoorOperationCommand.OPEN,
|
door=self.coordinator.data.doors[self.door_id],
|
||||||
)
|
operation=TailwindDoorOperationCommand.OPEN,
|
||||||
self._attr_is_opening = False
|
)
|
||||||
|
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()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||||
@ -71,9 +98,28 @@ class TailwindDoorCoverEntity(TailwindDoorEntity, CoverEntity):
|
|||||||
"""
|
"""
|
||||||
self._attr_is_closing = True
|
self._attr_is_closing = True
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
await self.coordinator.tailwind.operate(
|
try:
|
||||||
door=self.coordinator.data.doors[self.door_id],
|
await self.coordinator.tailwind.operate(
|
||||||
operation=TailwindDoorOperationCommand.CLOSE,
|
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
|
self._attr_is_closing = False
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
@ -5,12 +5,13 @@ from collections.abc import Awaitable, Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from gotailwind import Tailwind, TailwindDeviceStatus
|
from gotailwind import Tailwind, TailwindDeviceStatus, TailwindError
|
||||||
|
|
||||||
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import PERCENTAGE, EntityCategory
|
from homeassistant.const import PERCENTAGE, EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
@ -72,5 +73,12 @@ class TailwindNumberEntity(TailwindEntity, NumberEntity):
|
|||||||
|
|
||||||
async def async_set_native_value(self, value: float) -> None:
|
async def async_set_native_value(self, value: float) -> None:
|
||||||
"""Change to new number value."""
|
"""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()
|
await self.coordinator.async_request_refresh()
|
||||||
|
@ -60,5 +60,16 @@
|
|||||||
"name": "Status LED brightness"
|
"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."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
"""Tests for button entities provided by the Tailwind integration."""
|
"""Tests for button entities provided by the Tailwind integration."""
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from gotailwind import TailwindError
|
||||||
import pytest
|
import pytest
|
||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
|
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.const import ATTR_ENTITY_ID
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
|
|
||||||
pytestmark = [
|
pytestmark = [
|
||||||
@ -46,3 +49,17 @@ async def test_number_entities(
|
|||||||
|
|
||||||
assert (state := hass.states.get(state.entity_id))
|
assert (state := hass.states.get(state.entity_id))
|
||||||
assert state.state == "2023-12-17T15:25:00+00:00"
|
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"
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
"""Tests for cover entities provided by the Tailwind integration."""
|
"""Tests for cover entities provided by the Tailwind integration."""
|
||||||
from unittest.mock import ANY, MagicMock
|
from unittest.mock import ANY, MagicMock
|
||||||
|
|
||||||
from gotailwind import TailwindDoorOperationCommand
|
from gotailwind import (
|
||||||
|
TailwindDoorDisabledError,
|
||||||
|
TailwindDoorLockedOutError,
|
||||||
|
TailwindDoorOperationCommand,
|
||||||
|
TailwindError,
|
||||||
|
)
|
||||||
import pytest
|
import pytest
|
||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
@ -10,8 +15,10 @@ from homeassistant.components.cover import (
|
|||||||
SERVICE_CLOSE_COVER,
|
SERVICE_CLOSE_COVER,
|
||||||
SERVICE_OPEN_COVER,
|
SERVICE_OPEN_COVER,
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.tailwind.const import DOMAIN
|
||||||
from homeassistant.const import ATTR_ENTITY_ID
|
from homeassistant.const import ATTR_ENTITY_ID
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
|
|
||||||
pytestmark = pytest.mark.usefixtures("init_integration")
|
pytestmark = pytest.mark.usefixtures("init_integration")
|
||||||
@ -74,3 +81,90 @@ async def test_cover_operations(
|
|||||||
mock_tailwind.operate.assert_called_with(
|
mock_tailwind.operate.assert_called_with(
|
||||||
door=ANY, operation=TailwindDoorOperationCommand.CLOSE
|
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"
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
"""Tests for number entities provided by the Tailwind integration."""
|
"""Tests for number entities provided by the Tailwind integration."""
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from gotailwind import TailwindError
|
||||||
import pytest
|
import pytest
|
||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant.components import number
|
from homeassistant.components import number
|
||||||
from homeassistant.components.number import ATTR_VALUE, SERVICE_SET_VALUE
|
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.const import ATTR_ENTITY_ID
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
|
|
||||||
pytestmark = pytest.mark.usefixtures("init_integration")
|
pytestmark = pytest.mark.usefixtures("init_integration")
|
||||||
@ -44,3 +47,20 @@ async def test_number_entities(
|
|||||||
|
|
||||||
assert len(mock_tailwind.status_led.mock_calls) == 1
|
assert len(mock_tailwind.status_led.mock_calls) == 1
|
||||||
mock_tailwind.status_led.assert_called_with(brightness=42)
|
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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user