From cf914f559f0a6733cb2aef1f05f1bbf5ff2a4148 Mon Sep 17 00:00:00 2001 From: Tomasz Dylewski Date: Mon, 15 Jun 2026 19:47:03 +0200 Subject: [PATCH] Add battery sensor support for PAJ GPS devices (#173123) --- homeassistant/components/paj_gps/sensor.py | 18 ++++- .../paj_gps/fixtures/trackpoint.json | 2 +- .../paj_gps/snapshots/test_sensor.ambr | 62 +++++++++++++++- tests/components/paj_gps/test_sensor.py | 72 +++++++++++++++---- 4 files changed, 134 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/paj_gps/sensor.py b/homeassistant/components/paj_gps/sensor.py index ef3b5f74678..f6b912b4455 100644 --- a/homeassistant/components/paj_gps/sensor.py +++ b/homeassistant/components/paj_gps/sensor.py @@ -1,7 +1,7 @@ """Platform for PAJ GPS sensor integration.""" from collections.abc import Callable -from dataclasses import dataclass +from dataclasses import dataclass, field from pajgps_api.models.trackpoint import TrackPoint @@ -11,12 +11,12 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.const import UnitOfSpeed +from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfSpeed from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import PajGpsConfigEntry -from .coordinator import PajGpsCoordinator +from .coordinator import Device, PajGpsCoordinator from .entity import PajGpsEntity PARALLEL_UPDATES = 0 @@ -27,6 +27,7 @@ class PajGpsSensorEntityDescription(SensorEntityDescription): """Describes a PAJ GPS sensor entity.""" value_fn: Callable[[TrackPoint], int | None] + supported_fn: Callable[[Device], bool] = field(default=lambda _: True) SENSOR_DESCRIPTIONS: tuple[PajGpsSensorEntityDescription, ...] = ( @@ -38,6 +39,16 @@ SENSOR_DESCRIPTIONS: tuple[PajGpsSensorEntityDescription, ...] = ( suggested_display_precision=0, value_fn=lambda tp: tp.speed, ), + PajGpsSensorEntityDescription( + key="battery", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + suggested_display_precision=0, + value_fn=lambda tp: tp.battery_level, + supported_fn=lambda device: device.has_battery, + ), ) @@ -62,6 +73,7 @@ async def async_setup_entry( PajGpsSensor(coordinator, device_id, description) for device_id in sorted_new_ids for description in SENSOR_DESCRIPTIONS + if description.supported_fn(coordinator.data.devices[device_id]) ) known_device_ids.update(sorted_new_ids) diff --git a/tests/components/paj_gps/fixtures/trackpoint.json b/tests/components/paj_gps/fixtures/trackpoint.json index 605ed6fd484..0731b6f02c6 100644 --- a/tests/components/paj_gps/fixtures/trackpoint.json +++ b/tests/components/paj_gps/fixtures/trackpoint.json @@ -3,6 +3,6 @@ "lat": 52.0, "lng": 13.0, "speed": 50, - "battery": 80, + "battery_level": 80, "direction": 90 } diff --git a/tests/components/paj_gps/snapshots/test_sensor.ambr b/tests/components/paj_gps/snapshots/test_sensor.ambr index 0fb56573a90..9acae245823 100644 --- a/tests/components/paj_gps/snapshots/test_sensor.ambr +++ b/tests/components/paj_gps/snapshots/test_sensor.ambr @@ -1,5 +1,63 @@ # serializer version: 1 -# name: test_all_entities[sensor.device_1_speed-entry] +# name: test_battery_sensor_created_when_has_battery[sensor.device_1_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.device_1_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery', + 'platform': 'paj_gps', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '42_1_battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_battery_sensor_created_when_has_battery[sensor.device_1_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Device 1 Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.device_1_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80', + }) +# --- +# name: test_battery_sensor_created_when_has_battery[sensor.device_1_speed-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -41,7 +99,7 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[sensor.device_1_speed-state] +# name: test_battery_sensor_created_when_has_battery[sensor.device_1_speed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'speed', diff --git a/tests/components/paj_gps/test_sensor.py b/tests/components/paj_gps/test_sensor.py index a706e66535e..031dcfdeae0 100644 --- a/tests/components/paj_gps/test_sensor.py +++ b/tests/components/paj_gps/test_sensor.py @@ -5,17 +5,19 @@ from __future__ import annotations from collections.abc import Generator from unittest.mock import AsyncMock, patch +from pajgps_api.models.device import Device from pajgps_api.models.trackpoint import TrackPoint import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.paj_gps.const import DOMAIN from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from . import setup_integration -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import MockConfigEntry, load_json_object_fixture, snapshot_platform @pytest.fixture(autouse=True) @@ -28,19 +30,6 @@ def sensor_only() -> Generator[None]: yield -async def test_all_entities( - hass: HomeAssistant, - mock_paj_gps_api: AsyncMock, - mock_config_entry: MockConfigEntry, - entity_registry: er.EntityRegistry, - snapshot: SnapshotAssertion, -) -> None: - """Test all sensor entities against snapshot.""" - await setup_integration(hass, mock_config_entry) - - await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - - async def test_speed_none_when_missing( hass: HomeAssistant, mock_paj_gps_api: AsyncMock, @@ -56,3 +45,58 @@ async def test_speed_none_when_missing( state = hass.states.get("sensor.device_1_speed") assert state is not None assert state.state == STATE_UNKNOWN + + +@pytest.fixture +def mock_paj_gps_api_with_battery(mock_paj_gps_api: AsyncMock) -> AsyncMock: + """Override get_devices to return a device with a standalone battery.""" + mock_paj_gps_api.get_devices.return_value = [ + Device( + **{ + **load_json_object_fixture("device.json", DOMAIN), + "device_models": [{"standalone_battery": 1}], + } + ) + ] + return mock_paj_gps_api + + +@pytest.mark.usefixtures("mock_paj_gps_api_with_battery") +async def test_battery_sensor_created_when_has_battery( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test that a battery sensor is created for devices with a standalone battery.""" + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.usefixtures("mock_paj_gps_api") +async def test_battery_sensor_not_created_when_no_battery( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that no battery sensor is created for devices without a standalone battery.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("sensor.device_1_battery") is None + + +async def test_battery_none_when_missing( + hass: HomeAssistant, + mock_paj_gps_api_with_battery: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that battery state is unknown when the trackpoint has no battery_level value.""" + mock_paj_gps_api_with_battery.get_all_last_positions.return_value = [ + TrackPoint(iddevice=1, battery_level=None) + ] + + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("sensor.device_1_battery") + assert state is not None + assert state.state == STATE_UNKNOWN