From 4e852911aa01aba211d61ddcc9f058db5b88685e Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 19 Apr 2025 17:30:15 +0200 Subject: [PATCH] Add `tracker power saving` binary sensor to Tractive integration (#142718) * Add power saving binary sensor * Update tests * tracker_state_reason is not always present in hardware event --- homeassistant/components/tractive/__init__.py | 2 + .../components/tractive/binary_sensor.py | 38 +++++++++++---- homeassistant/components/tractive/const.py | 1 + .../components/tractive/strings.json | 3 ++ tests/components/tractive/conftest.py | 1 + .../snapshots/test_binary_sensor.ambr | 47 +++++++++++++++++++ 6 files changed, 82 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index 8bc2d11d047..60bae9bfd2e 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -31,6 +31,7 @@ from .const import ( ATTR_MINUTES_DAY_SLEEP, ATTR_MINUTES_NIGHT_SLEEP, ATTR_MINUTES_REST, + ATTR_POWER_SAVING, ATTR_SLEEP_LABEL, ATTR_TRACKER_STATE, CLIENT_ID, @@ -277,6 +278,7 @@ class TractiveClient: payload = { ATTR_BATTERY_LEVEL: event["hardware"]["battery_level"], ATTR_TRACKER_STATE: event["tracker_state"].lower(), + ATTR_POWER_SAVING: event.get("tracker_state_reason") == "POWER_SAVING", ATTR_BATTERY_CHARGING: event["charging_state"] == "CHARGING", } self._dispatch_tracker_event( diff --git a/homeassistant/components/tractive/binary_sensor.py b/homeassistant/components/tractive/binary_sensor.py index 2978d369344..9ded1f699c3 100644 --- a/homeassistant/components/tractive/binary_sensor.py +++ b/homeassistant/components/tractive/binary_sensor.py @@ -2,6 +2,8 @@ from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass from typing import Any from homeassistant.components.binary_sensor import ( @@ -14,7 +16,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import Trackables, TractiveClient, TractiveConfigEntry -from .const import TRACKER_HARDWARE_STATUS_UPDATED +from .const import ATTR_POWER_SAVING, TRACKER_HARDWARE_STATUS_UPDATED from .entity import TractiveEntity @@ -25,7 +27,7 @@ class TractiveBinarySensor(TractiveEntity, BinarySensorEntity): self, client: TractiveClient, item: Trackables, - description: BinarySensorEntityDescription, + description: TractiveBinarySensorEntityDescription, ) -> None: """Initialize sensor entity.""" super().__init__( @@ -47,12 +49,27 @@ class TractiveBinarySensor(TractiveEntity, BinarySensorEntity): super().handle_status_update(event) -SENSOR_TYPE = BinarySensorEntityDescription( - key=ATTR_BATTERY_CHARGING, - translation_key="tracker_battery_charging", - device_class=BinarySensorDeviceClass.BATTERY_CHARGING, - entity_category=EntityCategory.DIAGNOSTIC, -) +@dataclass(frozen=True, kw_only=True) +class TractiveBinarySensorEntityDescription(BinarySensorEntityDescription): + """Class describing Tractive binary sensor entities.""" + + supported: Callable[[dict], bool] = lambda _: True + + +SENSOR_TYPES = [ + TractiveBinarySensorEntityDescription( + key=ATTR_BATTERY_CHARGING, + translation_key="tracker_battery_charging", + device_class=BinarySensorDeviceClass.BATTERY_CHARGING, + entity_category=EntityCategory.DIAGNOSTIC, + supported=lambda details: details.get("charging_state") is not None, + ), + TractiveBinarySensorEntityDescription( + key=ATTR_POWER_SAVING, + translation_key="tracker_power_saving", + entity_category=EntityCategory.DIAGNOSTIC, + ), +] async def async_setup_entry( @@ -65,9 +82,10 @@ async def async_setup_entry( trackables = entry.runtime_data.trackables entities = [ - TractiveBinarySensor(client, item, SENSOR_TYPE) + TractiveBinarySensor(client, item, description) + for description in SENSOR_TYPES for item in trackables - if item.tracker_details.get("charging_state") is not None + if description.supported(item.tracker_details) ] async_add_entities(entities) diff --git a/homeassistant/components/tractive/const.py b/homeassistant/components/tractive/const.py index cb5d4066dd9..9b925015772 100644 --- a/homeassistant/components/tractive/const.py +++ b/homeassistant/components/tractive/const.py @@ -16,6 +16,7 @@ ATTR_MINUTES_ACTIVE = "minutes_active" ATTR_MINUTES_DAY_SLEEP = "minutes_day_sleep" ATTR_MINUTES_NIGHT_SLEEP = "minutes_night_sleep" ATTR_MINUTES_REST = "minutes_rest" +ATTR_POWER_SAVING = "power_saving" ATTR_SLEEP_LABEL = "sleep_label" ATTR_TRACKER_STATE = "tracker_state" diff --git a/homeassistant/components/tractive/strings.json b/homeassistant/components/tractive/strings.json index 0690328c99c..a56a2982057 100644 --- a/homeassistant/components/tractive/strings.json +++ b/homeassistant/components/tractive/strings.json @@ -22,6 +22,9 @@ "binary_sensor": { "tracker_battery_charging": { "name": "Tracker battery charging" + }, + "tracker_power_saving": { + "name": "Tracker power saving" } }, "device_tracker": { diff --git a/tests/components/tractive/conftest.py b/tests/components/tractive/conftest.py index 88c68a4b62f..f32aaa84349 100644 --- a/tests/components/tractive/conftest.py +++ b/tests/components/tractive/conftest.py @@ -29,6 +29,7 @@ def mock_tractive_client() -> Generator[AsyncMock]: "tracker_id": "device_id_123", "hardware": {"battery_level": 88}, "tracker_state": "operational", + "tracker_state_reason": "POWER_SAVING", "charging_state": "CHARGING", } entry.runtime_data.client._send_hardware_update(event) diff --git a/tests/components/tractive/snapshots/test_binary_sensor.ambr b/tests/components/tractive/snapshots/test_binary_sensor.ambr index 761626347a7..c7252da7a3b 100644 --- a/tests/components/tractive/snapshots/test_binary_sensor.ambr +++ b/tests/components/tractive/snapshots/test_binary_sensor.ambr @@ -47,3 +47,50 @@ 'state': 'on', }) # --- +# name: test_binary_sensor[binary_sensor.test_pet_tracker_power_saving-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_pet_tracker_power_saving', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tracker power saving', + 'platform': 'tractive', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tracker_power_saving', + 'unique_id': 'pet_id_123_power_saving', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_pet_tracker_power_saving-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Pet Tracker power saving', + }), + 'context': , + 'entity_id': 'binary_sensor.test_pet_tracker_power_saving', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# ---