From 0f079454bb4b5e5a8b6f00c9f258617da63abd6b Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 20 Jul 2024 22:37:57 +1000 Subject: [PATCH] Add device tracker to Tesla Fleet (#122222) --- .../components/tesla_fleet/__init__.py | 2 +- .../components/tesla_fleet/device_tracker.py | 106 ++++++++++++++++++ .../components/tesla_fleet/icons.json | 8 ++ .../components/tesla_fleet/strings.json | 8 ++ .../snapshots/test_device_tracker.ambr | 101 +++++++++++++++++ .../tesla_fleet/test_device_tracker.py | 37 ++++++ 6 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tesla_fleet/device_tracker.py create mode 100644 tests/components/tesla_fleet/snapshots/test_device_tracker.ambr create mode 100644 tests/components/tesla_fleet/test_device_tracker.py diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py index 0613f42ee61..892859cefd1 100644 --- a/homeassistant/components/tesla_fleet/__init__.py +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -34,7 +34,7 @@ from .coordinator import ( ) from .models import TeslaFleetData, TeslaFleetEnergyData, TeslaFleetVehicleData -PLATFORMS: Final = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS: Final = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER, Platform.SENSOR] type TeslaFleetConfigEntry = ConfigEntry[TeslaFleetData] diff --git a/homeassistant/components/tesla_fleet/device_tracker.py b/homeassistant/components/tesla_fleet/device_tracker.py new file mode 100644 index 00000000000..1d396286d7c --- /dev/null +++ b/homeassistant/components/tesla_fleet/device_tracker.py @@ -0,0 +1,106 @@ +"""Device Tracker platform for Tesla Fleet integration.""" + +from __future__ import annotations + +from homeassistant.components.device_tracker import SourceType +from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity + +from .entity import TeslaFleetVehicleEntity +from .models import TeslaFleetVehicleData + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Tesla Fleet device tracker platform from a config entry.""" + + async_add_entities( + klass(vehicle) + for klass in ( + TeslaFleetDeviceTrackerLocationEntity, + TeslaFleetDeviceTrackerRouteEntity, + ) + for vehicle in entry.runtime_data.vehicles + ) + + +class TeslaFleetDeviceTrackerEntity( + TeslaFleetVehicleEntity, TrackerEntity, RestoreEntity +): + """Base class for Tesla Fleet device tracker entities.""" + + _attr_latitude: float | None = None + _attr_longitude: float | None = None + + def __init__( + self, + vehicle: TeslaFleetVehicleData, + ) -> None: + """Initialize the device tracker.""" + super().__init__(vehicle, self.key) + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + if ( + (state := await self.async_get_last_state()) is not None + and self._attr_latitude is None + and self._attr_longitude is None + ): + self._attr_latitude = state.attributes.get("latitude") + self._attr_longitude = state.attributes.get("longitude") + + @property + def latitude(self) -> float | None: + """Return latitude value of the device.""" + return self._attr_latitude + + @property + def longitude(self) -> float | None: + """Return longitude value of the device.""" + return self._attr_longitude + + @property + def source_type(self) -> SourceType | str: + """Return the source type of the device tracker.""" + return SourceType.GPS + + +class TeslaFleetDeviceTrackerLocationEntity(TeslaFleetDeviceTrackerEntity): + """Vehicle Location device tracker Class.""" + + key = "location" + + def _async_update_attrs(self) -> None: + """Update the attributes of the entity.""" + + self._attr_latitude = self.get("drive_state_latitude") + self._attr_longitude = self.get("drive_state_longitude") + self._attr_available = not ( + self.get("drive_state_longitude", False) is None + or self.get("drive_state_latitude", False) is None + ) + + +class TeslaFleetDeviceTrackerRouteEntity(TeslaFleetDeviceTrackerEntity): + """Vehicle Navigation device tracker Class.""" + + key = "route" + + def _async_update_attrs(self) -> None: + """Update the attributes of the device tracker.""" + self._attr_latitude = self.get("drive_state_active_route_latitude") + self._attr_longitude = self.get("drive_state_active_route_longitude") + self._attr_available = not ( + self.get("drive_state_active_route_longitude", False) is None + or self.get("drive_state_active_route_latitude", False) is None + ) + + @property + def location_name(self) -> str | None: + """Return a location name for the current location of the device.""" + return self.get("drive_state_active_route_destination") diff --git a/homeassistant/components/tesla_fleet/icons.json b/homeassistant/components/tesla_fleet/icons.json index 5556219ed82..2dbde45ee08 100644 --- a/homeassistant/components/tesla_fleet/icons.json +++ b/homeassistant/components/tesla_fleet/icons.json @@ -38,6 +38,14 @@ } } }, + "device_tracker": { + "location": { + "default": "mdi:map-marker" + }, + "route": { + "default": "mdi:routes" + } + }, "sensor": { "battery_power": { "default": "mdi:home-battery" diff --git a/homeassistant/components/tesla_fleet/strings.json b/homeassistant/components/tesla_fleet/strings.json index 5b66879ac0d..6e74714ddd5 100644 --- a/homeassistant/components/tesla_fleet/strings.json +++ b/homeassistant/components/tesla_fleet/strings.json @@ -107,6 +107,14 @@ "name": "Tire pressure warning rear right" } }, + "device_tracker": { + "location": { + "name": "Location" + }, + "route": { + "name": "Route" + } + }, "sensor": { "battery_power": { "name": "Battery power" diff --git a/tests/components/tesla_fleet/snapshots/test_device_tracker.ambr b/tests/components/tesla_fleet/snapshots/test_device_tracker.ambr new file mode 100644 index 00000000000..194eda6fcff --- /dev/null +++ b/tests/components/tesla_fleet/snapshots/test_device_tracker.ambr @@ -0,0 +1,101 @@ +# serializer version: 1 +# name: test_device_tracker[device_tracker.test_location-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'device_tracker', + 'entity_category': , + 'entity_id': 'device_tracker.test_location', + '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': 'Location', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'location', + 'unique_id': 'LRWXF7EK4KC700000-location', + 'unit_of_measurement': None, + }) +# --- +# name: test_device_tracker[device_tracker.test_location-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Location', + 'gps_accuracy': 0, + 'latitude': -30.222626, + 'longitude': -97.6236871, + 'source_type': , + }), + 'context': , + 'entity_id': 'device_tracker.test_location', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'not_home', + }) +# --- +# name: test_device_tracker[device_tracker.test_route-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'device_tracker', + 'entity_category': , + 'entity_id': 'device_tracker.test_route', + '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': 'Route', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'route', + 'unique_id': 'LRWXF7EK4KC700000-route', + 'unit_of_measurement': None, + }) +# --- +# name: test_device_tracker[device_tracker.test_route-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Route', + 'gps_accuracy': 0, + 'latitude': 30.2226265, + 'longitude': -97.6236871, + 'source_type': , + }), + 'context': , + 'entity_id': 'device_tracker.test_route', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'not_home', + }) +# --- diff --git a/tests/components/tesla_fleet/test_device_tracker.py b/tests/components/tesla_fleet/test_device_tracker.py new file mode 100644 index 00000000000..66a0c06de7f --- /dev/null +++ b/tests/components/tesla_fleet/test_device_tracker.py @@ -0,0 +1,37 @@ +"""Test the Tesla Fleet device tracker platform.""" + +from syrupy import SnapshotAssertion +from tesla_fleet_api.exceptions import VehicleOffline + +from homeassistant.const import STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import assert_entities, setup_platform + +from tests.common import MockConfigEntry + + +async def test_device_tracker( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + normal_config_entry: MockConfigEntry, +) -> None: + """Tests that the device tracker entities are correct.""" + + await setup_platform(hass, normal_config_entry, [Platform.DEVICE_TRACKER]) + assert_entities(hass, normal_config_entry.entry_id, entity_registry, snapshot) + + +async def test_device_tracker_offline( + hass: HomeAssistant, + mock_vehicle_data, + normal_config_entry: MockConfigEntry, +) -> None: + """Tests that the device tracker entities are correct when offline.""" + + mock_vehicle_data.side_effect = VehicleOffline + await setup_platform(hass, normal_config_entry, [Platform.DEVICE_TRACKER]) + state = hass.states.get("device_tracker.test_location") + assert state.state == STATE_UNKNOWN