Add exception handling for updating LetPot time entities (#137033)

* Handle exceptions for entity edits for LetPot

* Set exception-translations: done
pull/137037/head
Joris Pelgröm 2025-01-31 21:28:23 +01:00 committed by GitHub
parent 164d38ac0d
commit 7103ea7e8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 94 additions and 3 deletions

View File

@ -1,5 +1,11 @@
"""Base class for LetPot entities.""" """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.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -23,3 +29,27 @@ class LetPotEntity(CoordinatorEntity[LetPotDeviceCoordinator]):
model_id=coordinator.device_client.device_model_code, model_id=coordinator.device_client.device_model_code,
serial_number=coordinator.device.serial_number, 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

View File

@ -29,7 +29,7 @@ rules:
unique-config-entry: done unique-config-entry: done
# Silver # Silver
action-exceptions: todo action-exceptions: done
config-entry-unloading: config-entry-unloading:
status: done status: done
comment: | comment: |
@ -63,7 +63,7 @@ rules:
entity-device-class: todo entity-device-class: todo
entity-disabled-by-default: todo entity-disabled-by-default: todo
entity-translations: done entity-translations: done
exception-translations: todo exception-translations: done
icon-translations: todo icon-translations: todo
reconfiguration-flow: todo reconfiguration-flow: todo
repair-issues: todo repair-issues: todo

View File

@ -40,5 +40,13 @@
"name": "Light on" "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}"
}
} }
} }

View File

@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import LetPotConfigEntry from . import LetPotConfigEntry
from .coordinator import LetPotDeviceCoordinator 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 # 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. # pending changes to avoid overwriting, but try to avoid a lot of parallelism.
@ -86,6 +86,7 @@ class LetPotTimeEntity(LetPotEntity, TimeEntity):
"""Return the time.""" """Return the time."""
return self.entity_description.value_fn(self.coordinator.data) return self.entity_description.value_fn(self.coordinator.data)
@exception_handler
async def async_set_value(self, value: time) -> None: async def async_set_value(self, value: time) -> None:
"""Set the time.""" """Set the time."""
await self.entity_description.set_value_fn( await self.entity_description.set_value_fn(

View File

@ -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"},
)