Skip to content

Commit

Permalink
Add exception handling for updating LetPot time entities (#137033)
Browse files Browse the repository at this point in the history
* Handle exceptions for entity edits for LetPot

* Set exception-translations: done
  • Loading branch information
jpelgrom authored Jan 31, 2025
1 parent 164d38a commit 7103ea7
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 3 deletions.
30 changes: 30 additions & 0 deletions homeassistant/components/letpot/entity.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
4 changes: 2 additions & 2 deletions homeassistant/components/letpot/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ rules:
unique-config-entry: done

# Silver
action-exceptions: todo
action-exceptions: done
config-entry-unloading:
status: done
comment: |
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions homeassistant/components/letpot/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
}
}
}
3 changes: 2 additions & 1 deletion homeassistant/components/letpot/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down
52 changes: 52 additions & 0 deletions tests/components/letpot/test_time.py
Original file line number Diff line number Diff line change
@@ -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"},
)

0 comments on commit 7103ea7

Please sign in to comment.