diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py index a425a26b6da..af2276dbcda 100644 --- a/homeassistant/components/teslemetry/__init__.py +++ b/homeassistant/components/teslemetry/__init__.py @@ -30,6 +30,7 @@ PLATFORMS: Final = [ Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.COVER, + Platform.DEVICE_TRACKER, Platform.LOCK, Platform.SELECT, Platform.SENSOR, diff --git a/homeassistant/components/teslemetry/device_tracker.py b/homeassistant/components/teslemetry/device_tracker.py new file mode 100644 index 00000000000..afd947ab3b3 --- /dev/null +++ b/homeassistant/components/teslemetry/device_tracker.py @@ -0,0 +1,85 @@ +"""Device tracker platform for Teslemetry 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 .entity import TeslemetryVehicleEntity +from .models import TeslemetryVehicleData + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Teslemetry device tracker platform from a config entry.""" + + async_add_entities( + klass(vehicle) + for klass in ( + TeslemetryDeviceTrackerLocationEntity, + TeslemetryDeviceTrackerRouteEntity, + ) + for vehicle in entry.runtime_data.vehicles + ) + + +class TeslemetryDeviceTrackerEntity(TeslemetryVehicleEntity, TrackerEntity): + """Base class for Teslemetry tracker entities.""" + + lat_key: str + lon_key: str + + def __init__( + self, + vehicle: TeslemetryVehicleData, + ) -> None: + """Initialize the device tracker.""" + super().__init__(vehicle, self.key) + + def _async_update_attrs(self) -> None: + """Update the attributes of the device tracker.""" + + self._attr_available = ( + self.get(self.lat_key, False) is not None + and self.get(self.lon_key, False) is not None + ) + + @property + def latitude(self) -> float | None: + """Return latitude value of the device.""" + return self.get(self.lat_key) + + @property + def longitude(self) -> float | None: + """Return longitude value of the device.""" + return self.get(self.lon_key) + + @property + def source_type(self) -> SourceType: + """Return the source type of the device tracker.""" + return SourceType.GPS + + +class TeslemetryDeviceTrackerLocationEntity(TeslemetryDeviceTrackerEntity): + """Vehicle location device tracker class.""" + + key = "location" + lat_key = "drive_state_latitude" + lon_key = "drive_state_longitude" + + +class TeslemetryDeviceTrackerRouteEntity(TeslemetryDeviceTrackerEntity): + """Vehicle navigation device tracker class.""" + + key = "route" + lat_key = "drive_state_active_route_latitude" + lon_key = "drive_state_active_route_longitude" + + @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/teslemetry/icons.json b/homeassistant/components/teslemetry/icons.json index 0236bc41c23..3224fee603b 100644 --- a/homeassistant/components/teslemetry/icons.json +++ b/homeassistant/components/teslemetry/icons.json @@ -109,6 +109,7 @@ "off": "mdi:car-seat" } }, + "components_customer_preferred_export_rule": { "default": "mdi:transmission-tower", "state": { @@ -126,6 +127,14 @@ } } }, + "device_tracker": { + "location": { + "default": "mdi:map-marker" + }, + "route": { + "default": "mdi:routes" + } + }, "cover": { "charge_state_charge_port_door_open": { "default": "mdi:ev-plug-ccs2" diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index 322a27929e5..e41fbbd4507 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -111,6 +111,14 @@ } } }, + "device_tracker": { + "location": { + "name": "Location" + }, + "route": { + "name": "Route" + } + }, "lock": { "charge_state_charge_port_latch": { "name": "Charge cable lock" diff --git a/tests/components/teslemetry/snapshots/test_device_tracker.ambr b/tests/components/teslemetry/snapshots/test_device_tracker.ambr new file mode 100644 index 00000000000..369a3e3a2b9 --- /dev/null +++ b/tests/components/teslemetry/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': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'location', + 'unique_id': 'VINVINVIN-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': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'route', + 'unique_id': 'VINVINVIN-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/teslemetry/test_device_tracker.py b/tests/components/teslemetry/test_device_tracker.py new file mode 100644 index 00000000000..55deaefdab5 --- /dev/null +++ b/tests/components/teslemetry/test_device_tracker.py @@ -0,0 +1,33 @@ +"""Test the Teslemetry 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 + + +async def test_device_tracker( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Tests that the device tracker entities are correct.""" + + entry = await setup_platform(hass, [Platform.DEVICE_TRACKER]) + assert_entities(hass, entry.entry_id, entity_registry, snapshot) + + +async def test_device_tracker_offline( + hass: HomeAssistant, + mock_vehicle_data, +) -> None: + """Tests that the device tracker entities are correct when offline.""" + + mock_vehicle_data.side_effect = VehicleOffline + await setup_platform(hass, [Platform.DEVICE_TRACKER]) + state = hass.states.get("device_tracker.test_location") + assert state.state == STATE_UNKNOWN