diff --git a/.coveragerc b/.coveragerc index 93958a67973..02b0cf7a143 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1187,6 +1187,7 @@ omit = homeassistant/components/starlink/binary_sensor.py homeassistant/components/starlink/button.py homeassistant/components/starlink/coordinator.py + homeassistant/components/starlink/device_tracker.py homeassistant/components/starlink/sensor.py homeassistant/components/starlink/switch.py homeassistant/components/starline/__init__.py diff --git a/homeassistant/components/starlink/__init__.py b/homeassistant/components/starlink/__init__.py index c59269d2e07..3413c4ff595 100644 --- a/homeassistant/components/starlink/__init__.py +++ b/homeassistant/components/starlink/__init__.py @@ -11,6 +11,7 @@ from .coordinator import StarlinkUpdateCoordinator PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.DEVICE_TRACKER, Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/starlink/coordinator.py b/homeassistant/components/starlink/coordinator.py index 3359706372e..95a5515ab21 100644 --- a/homeassistant/components/starlink/coordinator.py +++ b/homeassistant/components/starlink/coordinator.py @@ -10,8 +10,10 @@ from starlink_grpc import ( AlertDict, ChannelContext, GrpcError, + LocationDict, ObstructionDict, StatusDict, + location_data, reboot, set_stow_state, status_data, @@ -28,6 +30,7 @@ _LOGGER = logging.getLogger(__name__) class StarlinkData: """Contains data pulled from the Starlink system.""" + location: LocationDict status: StatusDict obstruction: ObstructionDict alert: AlertDict @@ -53,7 +56,10 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]): status = await self.hass.async_add_executor_job( status_data, self.channel_context ) - return StarlinkData(*status) + location = await self.hass.async_add_executor_job( + location_data, self.channel_context + ) + return StarlinkData(location, *status) except GrpcError as exc: raise UpdateFailed from exc diff --git a/homeassistant/components/starlink/device_tracker.py b/homeassistant/components/starlink/device_tracker.py new file mode 100644 index 00000000000..eb832741f40 --- /dev/null +++ b/homeassistant/components/starlink/device_tracker.py @@ -0,0 +1,73 @@ +"""Contains device trackers exposed by the Starlink integration.""" + +from collections.abc import Callable +from dataclasses import dataclass + +from homeassistant.components.device_tracker import SourceType, TrackerEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import StarlinkData +from .entity import StarlinkEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up all binary sensors for this entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + StarlinkDeviceTrackerEntity(coordinator, description) + for description in DEVICE_TRACKERS + ) + + +@dataclass +class StarlinkDeviceTrackerEntityDescriptionMixin: + """Describes a Starlink device tracker.""" + + latitude_fn: Callable[[StarlinkData], float] + longitude_fn: Callable[[StarlinkData], float] + + +@dataclass +class StarlinkDeviceTrackerEntityDescription( + EntityDescription, StarlinkDeviceTrackerEntityDescriptionMixin +): + """Describes a Starlink button entity.""" + + +DEVICE_TRACKERS = [ + StarlinkDeviceTrackerEntityDescription( + key="device_location", + translation_key="device_location", + entity_registry_enabled_default=False, + latitude_fn=lambda data: data.location["latitude"], + longitude_fn=lambda data: data.location["longitude"], + ), +] + + +class StarlinkDeviceTrackerEntity(StarlinkEntity, TrackerEntity): + """A TrackerEntity for Starlink devices. Handles creating unique IDs.""" + + entity_description: StarlinkDeviceTrackerEntityDescription + + @property + def source_type(self) -> SourceType | str: + """Return the source type, eg gps or router, of the device.""" + return SourceType.GPS + + @property + def latitude(self) -> float | None: + """Return latitude value of the device.""" + return self.entity_description.latitude_fn(self.coordinator.data) + + @property + def longitude(self) -> float | None: + """Return longitude value of the device.""" + return self.entity_description.longitude_fn(self.coordinator.data) diff --git a/homeassistant/components/starlink/diagnostics.py b/homeassistant/components/starlink/diagnostics.py index 10711e7155e..88e6485cf77 100644 --- a/homeassistant/components/starlink/diagnostics.py +++ b/homeassistant/components/starlink/diagnostics.py @@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import StarlinkUpdateCoordinator -TO_REDACT = {"id"} +TO_REDACT = {"id", "latitude", "longitude", "altitude"} async def async_get_config_entry_diagnostics( diff --git a/homeassistant/components/starlink/strings.json b/homeassistant/components/starlink/strings.json index a9e50f5d39f..0ec85c68956 100644 --- a/homeassistant/components/starlink/strings.json +++ b/homeassistant/components/starlink/strings.json @@ -44,6 +44,11 @@ "name": "Unexpected location" } }, + "device_tracker": { + "device_location": { + "name": "Device location" + } + }, "sensor": { "ping": { "name": "Ping" diff --git a/tests/components/starlink/fixtures/location_data_success.json b/tests/components/starlink/fixtures/location_data_success.json new file mode 100644 index 00000000000..4d18d22d12e --- /dev/null +++ b/tests/components/starlink/fixtures/location_data_success.json @@ -0,0 +1,5 @@ +{ + "latitude": 37.422, + "longitude": -122.084, + "altitude": 100 +} diff --git a/tests/components/starlink/patchers.py b/tests/components/starlink/patchers.py index dfc0d2415df..d83451ecc17 100644 --- a/tests/components/starlink/patchers.py +++ b/tests/components/starlink/patchers.py @@ -8,11 +8,16 @@ SETUP_ENTRY_PATCHER = patch( "homeassistant.components.starlink.async_setup_entry", return_value=True ) -COORDINATOR_SUCCESS_PATCHER = patch( +STATUS_DATA_SUCCESS_PATCHER = patch( "homeassistant.components.starlink.coordinator.status_data", return_value=json.loads(load_fixture("status_data_success.json", "starlink")), ) +LOCATION_DATA_SUCCESS_PATCHER = patch( + "homeassistant.components.starlink.coordinator.location_data", + return_value=json.loads(load_fixture("location_data_success.json", "starlink")), +) + DEVICE_FOUND_PATCHER = patch( "homeassistant.components.starlink.config_flow.get_id", return_value="some-valid-id" ) diff --git a/tests/components/starlink/snapshots/test_diagnostics.ambr b/tests/components/starlink/snapshots/test_diagnostics.ambr index 6f859aaf50d..3bb7f235017 100644 --- a/tests/components/starlink/snapshots/test_diagnostics.ambr +++ b/tests/components/starlink/snapshots/test_diagnostics.ambr @@ -16,6 +16,11 @@ 'alert_thermal_throttle': False, 'alert_unexpected_location': False, }), + 'location': dict({ + 'altitude': '**REDACTED**', + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + }), 'obstruction': dict({ 'raw_wedges_fraction_obstructed[]': list([ None, diff --git a/tests/components/starlink/test_diagnostics.py b/tests/components/starlink/test_diagnostics.py index 4bf8a619c88..231b58a2d5e 100644 --- a/tests/components/starlink/test_diagnostics.py +++ b/tests/components/starlink/test_diagnostics.py @@ -5,7 +5,7 @@ from homeassistant.components.starlink.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant -from .patchers import COORDINATOR_SUCCESS_PATCHER +from .patchers import LOCATION_DATA_SUCCESS_PATCHER, STATUS_DATA_SUCCESS_PATCHER from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -23,7 +23,7 @@ async def test_diagnostics( data={CONF_IP_ADDRESS: "1.2.3.4:0000"}, ) - with COORDINATOR_SUCCESS_PATCHER: + with STATUS_DATA_SUCCESS_PATCHER, LOCATION_DATA_SUCCESS_PATCHER: entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/starlink/test_init.py b/tests/components/starlink/test_init.py index 72d3be52b4a..94a8a2a341b 100644 --- a/tests/components/starlink/test_init.py +++ b/tests/components/starlink/test_init.py @@ -4,7 +4,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant -from .patchers import COORDINATOR_SUCCESS_PATCHER +from .patchers import LOCATION_DATA_SUCCESS_PATCHER, STATUS_DATA_SUCCESS_PATCHER from tests.common import MockConfigEntry @@ -16,7 +16,7 @@ async def test_successful_entry(hass: HomeAssistant) -> None: data={CONF_IP_ADDRESS: "1.2.3.4:0000"}, ) - with COORDINATOR_SUCCESS_PATCHER: + with STATUS_DATA_SUCCESS_PATCHER, LOCATION_DATA_SUCCESS_PATCHER: entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -33,7 +33,7 @@ async def test_unload_entry(hass: HomeAssistant) -> None: data={CONF_IP_ADDRESS: "1.2.3.4:0000"}, ) - with COORDINATOR_SUCCESS_PATCHER: + with STATUS_DATA_SUCCESS_PATCHER, LOCATION_DATA_SUCCESS_PATCHER: entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id)