raise proper service exceptions in fressnapf_tracker (#159707)

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Kevin Stillhammer
2026-01-02 19:53:16 +01:00
committed by GitHub
parent bfef048a7c
commit afc256622a
8 changed files with 169 additions and 37 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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

View File

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

View File

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