diff --git a/homeassistant/components/subaru/const.py b/homeassistant/components/subaru/const.py index 42badfc0185..9c94ed35361 100644 --- a/homeassistant/components/subaru/const.py +++ b/homeassistant/components/subaru/const.py @@ -37,6 +37,7 @@ API_GEN_3 = "g3" MANUFACTURER = "Subaru" PLATFORMS = [ + Platform.DEVICE_TRACKER, Platform.LOCK, Platform.SENSOR, ] diff --git a/homeassistant/components/subaru/device_tracker.py b/homeassistant/components/subaru/device_tracker.py new file mode 100644 index 00000000000..4a8cb8ad5ee --- /dev/null +++ b/homeassistant/components/subaru/device_tracker.py @@ -0,0 +1,91 @@ +"""Support for Subaru device tracker.""" +from __future__ import annotations + +from typing import Any + +from subarulink.const import LATITUDE, LONGITUDE, TIMESTAMP + +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.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from . import get_device_info +from .const import ( + DOMAIN, + ENTRY_COORDINATOR, + ENTRY_VEHICLES, + VEHICLE_HAS_REMOTE_SERVICE, + VEHICLE_STATUS, + VEHICLE_VIN, +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Subaru device tracker by config_entry.""" + entry: dict = hass.data[DOMAIN][config_entry.entry_id] + coordinator: DataUpdateCoordinator = entry[ENTRY_COORDINATOR] + vehicle_info: dict = entry[ENTRY_VEHICLES] + entities: list[SubaruDeviceTracker] = [] + for vehicle in vehicle_info.values(): + if vehicle[VEHICLE_HAS_REMOTE_SERVICE]: + entities.append(SubaruDeviceTracker(vehicle, coordinator)) + async_add_entities(entities) + + +class SubaruDeviceTracker( + CoordinatorEntity[DataUpdateCoordinator[dict[str, Any]]], TrackerEntity +): + """Class for Subaru device tracker.""" + + _attr_icon = "mdi:car" + _attr_has_entity_name = True + _attr_name = None + + def __init__(self, vehicle_info: dict, coordinator: DataUpdateCoordinator) -> None: + """Initialize the device tracker.""" + super().__init__(coordinator) + self.vin = vehicle_info[VEHICLE_VIN] + self._attr_device_info = get_device_info(vehicle_info) + self._attr_unique_id = f"{self.vin}_location" + + @property + def extra_state_attributes(self) -> dict[str, Any] | None: + """Return entity specific state attributes.""" + return { + "Position timestamp": self.coordinator.data[self.vin][VEHICLE_STATUS].get( + TIMESTAMP + ) + } + + @property + def latitude(self) -> float | None: + """Return latitude value of the vehicle.""" + return self.coordinator.data[self.vin][VEHICLE_STATUS].get(LATITUDE) + + @property + def longitude(self) -> float | None: + """Return longitude value of the vehicle.""" + return self.coordinator.data[self.vin][VEHICLE_STATUS].get(LONGITUDE) + + @property + def source_type(self) -> SourceType: + """Return the source type of the vehicle.""" + return SourceType.GPS + + @property + def available(self) -> bool: + """Return if entity is available.""" + if vehicle_data := self.coordinator.data.get(self.vin): + if status := vehicle_data.get(VEHICLE_STATUS): + return status.keys() & {LATITUDE, LONGITUDE, TIMESTAMP} + return False diff --git a/tests/components/subaru/test_device_tracker.py b/tests/components/subaru/test_device_tracker.py new file mode 100644 index 00000000000..6bef5dc1c2c --- /dev/null +++ b/tests/components/subaru/test_device_tracker.py @@ -0,0 +1,60 @@ +"""Test Subaru device tracker.""" +from copy import deepcopy +from unittest.mock import patch + +from subarulink.const import LATITUDE, LONGITUDE, TIMESTAMP, VEHICLE_STATUS + +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .api_responses import EXPECTED_STATE_EV_IMPERIAL, VEHICLE_STATUS_EV +from .conftest import ( + MOCK_API_FETCH, + MOCK_API_GET_DATA, + advance_time_to_next_fetch, +) + +DEVICE_ID = "device_tracker.test_vehicle_2" + + +async def test_device_tracker(hass: HomeAssistant, ev_entry) -> None: + """Test subaru device tracker entity exists and has correct info.""" + entity_registry = er.async_get(hass) + entry = entity_registry.async_get(DEVICE_ID) + assert entry + actual = hass.states.get(DEVICE_ID) + assert ( + actual.attributes.get(ATTR_LONGITUDE) == EXPECTED_STATE_EV_IMPERIAL[LONGITUDE] + ) + assert actual.attributes.get(ATTR_LATITUDE) == EXPECTED_STATE_EV_IMPERIAL[LATITUDE] + + +async def test_device_tracker_none_data(hass: HomeAssistant, ev_entry) -> None: + """Test when location information contains None.""" + bad_status = deepcopy(VEHICLE_STATUS_EV) + bad_status[VEHICLE_STATUS][LATITUDE] = None + bad_status[VEHICLE_STATUS][LONGITUDE] = None + bad_status[VEHICLE_STATUS][TIMESTAMP] = None + with patch(MOCK_API_FETCH), patch(MOCK_API_GET_DATA, return_value=bad_status): + advance_time_to_next_fetch(hass) + await hass.async_block_till_done() + + actual = hass.states.get(DEVICE_ID) + assert not actual.attributes.get(ATTR_LATITUDE) + assert not actual.attributes.get(ATTR_LONGITUDE) + + +async def test_device_tracker_missing_data(hass: HomeAssistant, ev_entry) -> None: + """Test when location keys are missing from vehicle status.""" + bad_status = deepcopy(VEHICLE_STATUS_EV) + bad_status[VEHICLE_STATUS].pop(LATITUDE) + bad_status[VEHICLE_STATUS].pop(LONGITUDE) + bad_status[VEHICLE_STATUS].pop(TIMESTAMP) + with patch(MOCK_API_FETCH), patch(MOCK_API_GET_DATA, return_value=bad_status): + advance_time_to_next_fetch(hass) + await hass.async_block_till_done() + + actual = hass.states.get(DEVICE_ID) + assert not actual.attributes.get(ATTR_LATITUDE) + assert not actual.attributes.get(ATTR_LONGITUDE)