Add sensor platform to fressnapf_tracker (#157658)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Kevin Stillhammer
2025-12-02 15:33:57 +01:00
committed by GitHub
parent e2acf30637
commit c866dc973c
10 changed files with 232 additions and 48 deletions

View File

@@ -12,7 +12,7 @@ from .coordinator import (
FressnapfTrackerDataUpdateCoordinator,
)
PLATFORMS: list[Platform] = [Platform.DEVICE_TRACKER]
PLATFORMS: list[Platform] = [Platform.DEVICE_TRACKER, Platform.SENSOR]
async def async_setup_entry(

View File

@@ -1,6 +1,7 @@
"""fressnapf_tracker class."""
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import FressnapfTrackerDataUpdateCoordinator
@@ -25,3 +26,17 @@ class FressnapfTrackerBaseEntity(
manufacturer="Fressnapf",
serial_number=str(self.id),
)
class FressnapfTrackerEntity(FressnapfTrackerBaseEntity):
"""Entity for fressnapf_tracker."""
def __init__(
self,
coordinator: FressnapfTrackerDataUpdateCoordinator,
entity_description: EntityDescription,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self.entity_description = entity_description
self._attr_unique_id = f"{self.id}_{entity_description.key}"

View File

@@ -53,9 +53,7 @@ rules:
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations:
status: exempt
comment: No entities to translate
entity-translations: done
exception-translations: todo
icon-translations: todo
reconfiguration-flow: done

View File

@@ -0,0 +1,63 @@
"""Sensor platform for fressnapf_tracker."""
from collections.abc import Callable
from dataclasses import dataclass
from fressnapftracker import Tracker
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FressnapfTrackerConfigEntry
from .entity import FressnapfTrackerEntity
@dataclass(frozen=True, kw_only=True)
class FressnapfTrackerSensorDescription(SensorEntityDescription):
"""Class describing Fressnapf Tracker sensor entities."""
value_fn: Callable[[Tracker], int]
SENSOR_ENTITY_DESCRIPTIONS: tuple[FressnapfTrackerSensorDescription, ...] = (
FressnapfTrackerSensorDescription(
key="battery",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.battery,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: FressnapfTrackerConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Fressnapf Tracker sensors."""
async_add_entities(
FressnapfTrackerSensor(coordinator, sensor_description)
for sensor_description in SENSOR_ENTITY_DESCRIPTIONS
for coordinator in entry.runtime_data
)
class FressnapfTrackerSensor(FressnapfTrackerEntity, SensorEntity):
"""fressnapf_tracker sensor for general information."""
entity_description: FressnapfTrackerSensorDescription
@property
def native_value(self) -> int:
"""Return the state of the resources if it has been received yet."""
return self.entity_description.value_fn(self.coordinator.data)

View File

@@ -1,35 +1,4 @@
# serializer version: 1
# name: test_state_entity_device_snapshots[Fluffy-entry]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'fressnapf_tracker',
'ABC123456',
),
}),
'labels': set({
}),
'manufacturer': 'Fressnapf',
'model': 'GPS Tracker 2.0',
'model_id': None,
'name': 'Fluffy',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': 'ABC123456',
'sw_version': None,
'via_device_id': None,
})
# ---
# name: test_state_entity_device_snapshots[device_tracker.fluffy-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -0,0 +1,32 @@
# serializer version: 1
# name: test_state_entity_device_snapshots[Fluffy-entry]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'fressnapf_tracker',
'ABC123456',
),
}),
'labels': set({
}),
'manufacturer': 'Fressnapf',
'model': 'GPS Tracker 2.0',
'model_id': None,
'name': 'Fluffy',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': 'ABC123456',
'sw_version': None,
'via_device_id': None,
})
# ---

View File

@@ -0,0 +1,54 @@
# serializer version: 1
# name: test_state_entity_device_snapshots[sensor.fluffy_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.fluffy_battery',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'Battery',
'platform': 'fressnapf_tracker',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'ABC123456_battery',
'unit_of_measurement': '%',
})
# ---
# name: test_state_entity_device_snapshots[sensor.fluffy_battery-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'Fluffy Battery',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.fluffy_battery',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '85',
})
# ---

View File

@@ -1,38 +1,39 @@
"""Test the Fressnapf Tracker device tracker platform."""
from unittest.mock import AsyncMock, MagicMock
from collections.abc import AsyncGenerator
from unittest.mock import AsyncMock, MagicMock, patch
from fressnapftracker import Tracker
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.const import STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry, snapshot_platform
@pytest.fixture(autouse=True)
async def platforms() -> AsyncGenerator[None]:
"""Return the platforms to be loaded for this test."""
with patch(
"homeassistant.components.fressnapf_tracker.PLATFORMS",
[Platform.DEVICE_TRACKER],
):
yield
@pytest.mark.usefixtures("init_integration")
async def test_state_entity_device_snapshots(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test device tracker entity is created correctly."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
device_entries = dr.async_entries_for_config_entry(
device_registry, mock_config_entry.entry_id
)
assert device_entries
for device_entry in device_entries:
assert device_entry == snapshot(name=f"{device_entry.name}-entry"), (
f"device entry snapshot failed for {device_entry.name}"
)
@pytest.mark.usefixtures("mock_auth_client")
async def test_device_tracker_no_position(

View File

@@ -3,9 +3,11 @@
from unittest.mock import AsyncMock, MagicMock
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from tests.common import MockConfigEntry
@@ -59,3 +61,20 @@ async def test_setup_entry_api_error(
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
@pytest.mark.usefixtures("init_integration")
async def test_state_entity_device_snapshots(
device_registry: dr.DeviceRegistry,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test sensor entity is created correctly."""
device_entries = dr.async_entries_for_config_entry(
device_registry, mock_config_entry.entry_id
)
assert device_entries
for device_entry in device_entries:
assert device_entry == snapshot(name=f"{device_entry.name}-entry"), (
f"device entry snapshot failed for {device_entry.name}"
)

View File

@@ -0,0 +1,33 @@
"""Test the Fressnapf Tracker sensor platform."""
from collections.abc import AsyncGenerator
from unittest.mock import patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry, snapshot_platform
@pytest.fixture(autouse=True)
async def platforms() -> AsyncGenerator[None]:
"""Return the platforms to be loaded for this test."""
with patch(
"homeassistant.components.fressnapf_tracker.PLATFORMS", [Platform.SENSOR]
):
yield
@pytest.mark.usefixtures("init_integration")
async def test_state_entity_device_snapshots(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test sensor entity is created correctly."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)