diff --git a/homeassistant/components/fressnapf_tracker/light.py b/homeassistant/components/fressnapf_tracker/light.py index fc3c58445b3..363a41ad1ae 100644 --- a/homeassistant/components/fressnapf_tracker/light.py +++ b/homeassistant/components/fressnapf_tracker/light.py @@ -2,6 +2,8 @@ from typing import TYPE_CHECKING, Any +from fressnapftracker import FressnapfTrackerError + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ColorMode, @@ -16,6 +18,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import FressnapfTrackerConfigEntry from .const import DOMAIN from .entity import FressnapfTrackerEntity +from .services import handle_fressnapf_tracker_exception PARALLEL_UPDATES = 1 @@ -61,12 +64,18 @@ class FressnapfTrackerLight(FressnapfTrackerEntity, LightEntity): self.raise_if_not_activatable() brightness = kwargs.get(ATTR_BRIGHTNESS, 255) brightness = int((brightness / 255) * 100) - await self.coordinator.client.set_led_brightness(brightness) + try: + await self.coordinator.client.set_led_brightness(brightness) + except FressnapfTrackerError as e: + handle_fressnapf_tracker_exception(e) await self.coordinator.async_request_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the device.""" - await self.coordinator.client.set_led_brightness(0) + try: + await self.coordinator.client.set_led_brightness(0) + except FressnapfTrackerError as e: + handle_fressnapf_tracker_exception(e) await self.coordinator.async_request_refresh() def raise_if_not_activatable(self) -> None: diff --git a/homeassistant/components/fressnapf_tracker/quality_scale.yaml b/homeassistant/components/fressnapf_tracker/quality_scale.yaml index f4d24e577c7..39614e94b66 100644 --- a/homeassistant/components/fressnapf_tracker/quality_scale.yaml +++ b/homeassistant/components/fressnapf_tracker/quality_scale.yaml @@ -26,7 +26,7 @@ rules: unique-config-entry: done # Silver - action-exceptions: todo + action-exceptions: done config-entry-unloading: done docs-configuration-parameters: done docs-installation-parameters: done diff --git a/homeassistant/components/fressnapf_tracker/services.py b/homeassistant/components/fressnapf_tracker/services.py new file mode 100644 index 00000000000..267a25c1cf6 --- /dev/null +++ b/homeassistant/components/fressnapf_tracker/services.py @@ -0,0 +1,21 @@ +"""Services and service helpers for fressnapf_tracker.""" + +from fressnapftracker import FressnapfTrackerError, FressnapfTrackerInvalidTokenError + +from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError + +from .const import DOMAIN + + +def handle_fressnapf_tracker_exception(exception: FressnapfTrackerError): + """Handle the different FressnapfTracker errors.""" + if isinstance(exception, FressnapfTrackerInvalidTokenError): + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="invalid_auth", + ) from exception + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="api_error", + translation_placeholders={"error_message": str(exception)}, + ) from exception diff --git a/homeassistant/components/fressnapf_tracker/strings.json b/homeassistant/components/fressnapf_tracker/strings.json index 2cc88af8a8f..73be1adb376 100644 --- a/homeassistant/components/fressnapf_tracker/strings.json +++ b/homeassistant/components/fressnapf_tracker/strings.json @@ -77,6 +77,9 @@ } }, "exceptions": { + "api_error": { + "message": "An error occurred while communicating with the Fressnapf Tracker API: {error_message}" + }, "charging": { "message": "The flashlight cannot be activated while charging." }, diff --git a/homeassistant/components/fressnapf_tracker/switch.py b/homeassistant/components/fressnapf_tracker/switch.py index aebc7eb4873..5b2d52e60dd 100644 --- a/homeassistant/components/fressnapf_tracker/switch.py +++ b/homeassistant/components/fressnapf_tracker/switch.py @@ -2,6 +2,8 @@ from typing import TYPE_CHECKING, Any +from fressnapftracker import FressnapfTrackerError + from homeassistant.components.switch import ( SwitchDeviceClass, SwitchEntity, @@ -13,6 +15,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import FressnapfTrackerConfigEntry from .entity import FressnapfTrackerEntity +from .services import handle_fressnapf_tracker_exception PARALLEL_UPDATES = 1 @@ -43,12 +46,18 @@ class FressnapfTrackerSwitch(FressnapfTrackerEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the device.""" - await self.coordinator.client.set_energy_saving(True) + try: + await self.coordinator.client.set_energy_saving(True) + except FressnapfTrackerError as e: + handle_fressnapf_tracker_exception(e) await self.coordinator.async_request_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the device.""" - await self.coordinator.client.set_energy_saving(False) + try: + await self.coordinator.client.set_energy_saving(False) + except FressnapfTrackerError as e: + handle_fressnapf_tracker_exception(e) await self.coordinator.async_request_refresh() @property diff --git a/tests/components/fressnapf_tracker/conftest.py b/tests/components/fressnapf_tracker/conftest.py index 022490205e0..8e32084b94a 100644 --- a/tests/components/fressnapf_tracker/conftest.py +++ b/tests/components/fressnapf_tracker/conftest.py @@ -33,34 +33,6 @@ MOCK_USER_ID = 12345 MOCK_ACCESS_TOKEN = "mock_access_token" MOCK_SERIAL_NUMBER = "ABC123456" MOCK_DEVICE_TOKEN = "mock_device_token" -MOCK_TRACKER = Tracker( - name="Fluffy", - battery=85, - charging=False, - position=Position( - lat=52.520008, - lng=13.404954, - accuracy=10, - timestamp="2024-01-15T12:00:00Z", - ), - tracker_settings=TrackerSettings( - generation="GPS Tracker 2.0", - features=TrackerFeatures( - flash_light=True, energy_saving_mode=True, live_tracking=True - ), - ), - led_brightness=LedBrightness(status="ok", value=50), - energy_saving=EnergySaving(status="ok", value=1), - deep_sleep=None, - led_activatable=LedActivatable( - has_led=True, - seen_recently=True, - nonempty_battery=True, - not_charging=True, - overall=True, - ), - icon="http://res.cloudinary.com/iot-venture/image/upload/v1717594357/kyaqq7nfitrdvaoakb8s.jpg", -) @pytest.fixture @@ -136,7 +108,36 @@ def mock_api_client() -> Generator[MagicMock]: "homeassistant.components.fressnapf_tracker.coordinator.ApiClient" ) as mock_api_client: client = mock_api_client.return_value - client.get_tracker = AsyncMock(return_value=MOCK_TRACKER) + client.get_tracker = AsyncMock( + return_value=Tracker( + name="Fluffy", + battery=85, + charging=False, + position=Position( + lat=52.520008, + lng=13.404954, + accuracy=10, + timestamp="2024-01-15T12:00:00Z", + ), + tracker_settings=TrackerSettings( + generation="GPS Tracker 2.0", + features=TrackerFeatures( + flash_light=True, energy_saving_mode=True, live_tracking=True + ), + ), + led_brightness=LedBrightness(status="ok", value=50), + energy_saving=EnergySaving(status="ok", value=1), + deep_sleep=None, + led_activatable=LedActivatable( + has_led=True, + seen_recently=True, + nonempty_battery=True, + not_charging=True, + overall=True, + ), + icon="http://res.cloudinary.com/iot-venture/image/upload/v1717594357/kyaqq7nfitrdvaoakb8s.jpg", + ) + ) client.set_led_brightness = AsyncMock(return_value=None) client.set_energy_saving = AsyncMock(return_value=None) yield client diff --git a/tests/components/fressnapf_tracker/test_light.py b/tests/components/fressnapf_tracker/test_light.py index e124429e367..5c2ba227d61 100644 --- a/tests/components/fressnapf_tracker/test_light.py +++ b/tests/components/fressnapf_tracker/test_light.py @@ -3,7 +3,13 @@ from collections.abc import AsyncGenerator from unittest.mock import MagicMock, patch -from fressnapftracker import Tracker, TrackerFeatures, TrackerSettings +from fressnapftracker import ( + FressnapfTrackerError, + FressnapfTrackerInvalidTokenError, + Tracker, + TrackerFeatures, + TrackerSettings, +) import pytest from syrupy.assertion import SnapshotAssertion @@ -15,7 +21,7 @@ from homeassistant.components.light import ( ) from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry, snapshot_platform @@ -172,3 +178,41 @@ async def test_turn_on_led_not_activatable( ) mock_api_client.set_led_brightness.assert_not_called() + + +@pytest.mark.parametrize( + ("api_exception", "expected_exception"), + [ + (FressnapfTrackerError("Something went wrong"), HomeAssistantError), + ( + FressnapfTrackerInvalidTokenError("Token no longer valid"), + ConfigEntryAuthFailed, + ), + ], +) +@pytest.mark.parametrize("service", [SERVICE_TURN_ON, SERVICE_TURN_OFF]) +@pytest.mark.usefixtures("mock_auth_client") +async def test_turn_on_off_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_api_client: MagicMock, + api_exception: FressnapfTrackerError, + expected_exception: type[HomeAssistantError], + service: str, +) -> None: + """Test that errors during service handling are handled correctly.""" + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + entity_id = "light.fluffy_flashlight" + + mock_api_client.set_led_brightness.side_effect = api_exception + with pytest.raises(expected_exception): + await hass.services.async_call( + LIGHT_DOMAIN, + service, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) diff --git a/tests/components/fressnapf_tracker/test_switch.py b/tests/components/fressnapf_tracker/test_switch.py index 59b2cbb62db..d64384d3cbf 100644 --- a/tests/components/fressnapf_tracker/test_switch.py +++ b/tests/components/fressnapf_tracker/test_switch.py @@ -3,7 +3,13 @@ from collections.abc import AsyncGenerator from unittest.mock import MagicMock, patch -from fressnapftracker import Tracker, TrackerFeatures, TrackerSettings +from fressnapftracker import ( + FressnapfTrackerError, + FressnapfTrackerInvalidTokenError, + Tracker, + TrackerFeatures, + TrackerSettings, +) import pytest from syrupy.assertion import SnapshotAssertion @@ -14,6 +20,7 @@ from homeassistant.components.switch import ( ) from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry, snapshot_platform @@ -112,3 +119,41 @@ async def test_turn_off( ) mock_api_client.set_energy_saving.assert_called_once_with(False) + + +@pytest.mark.parametrize( + ("api_exception", "expected_exception"), + [ + (FressnapfTrackerError("Something went wrong"), HomeAssistantError), + ( + FressnapfTrackerInvalidTokenError("Token no longer valid"), + ConfigEntryAuthFailed, + ), + ], +) +@pytest.mark.parametrize("service", [SERVICE_TURN_ON, SERVICE_TURN_OFF]) +@pytest.mark.usefixtures("mock_auth_client") +async def test_turn_on_off_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_api_client: MagicMock, + api_exception: FressnapfTrackerError, + expected_exception: type[HomeAssistantError], + service: str, +) -> None: + """Test that errors during service handling are handled correctly.""" + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + entity_id = "switch.fluffy_sleep_mode" + + mock_api_client.set_energy_saving.side_effect = api_exception + with pytest.raises(expected_exception): + await hass.services.async_call( + SWITCH_DOMAIN, + service, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + )