From 120e94cf0e272fe88973b866151b02c2b173ee5d Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 30 Sep 2026 19:28:31 +1000 Subject: [PATCH] Require the location scope for Teslemetry location sensors (#183467) --- .../components/teslemetry/binary_sensor.py | 6 ++++ homeassistant/components/teslemetry/sensor.py | 7 ++++ .../teslemetry/test_binary_sensor.py | 35 ++++++++++++++++++- tests/components/teslemetry/test_sensor.py | 33 +++++++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/teslemetry/binary_sensor.py b/homeassistant/components/teslemetry/binary_sensor.py index ba18040ffa55..1995dadf095f 100644 --- a/homeassistant/components/teslemetry/binary_sensor.py +++ b/homeassistant/components/teslemetry/binary_sensor.py @@ -5,6 +5,7 @@ from dataclasses import dataclass from typing import cast, override from tesla_fleet_api import firmware_at_least +from tesla_fleet_api.const import Scope from teslemetry_stream.vehicle import TeslemetryStreamVehicle from homeassistant.components.binary_sensor import ( @@ -52,6 +53,7 @@ class TeslemetryBinarySensorEntityDescription(BinarySensorEntityDescription): | None ) = None streaming_firmware: str = "2024.26" + requires_location_scope: bool = False VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = ( @@ -351,6 +353,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = ( ), TeslemetryBinarySensorEntityDescription( key="gps_state", + requires_location_scope=True, streaming_listener=lambda vehicle, callback: vehicle.listen_GpsState(callback), entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -556,9 +559,12 @@ async def async_setup_entry( ) -> None: """Set up the Teslemetry binary sensor platform from a config entry.""" + location_scope = Scope.VEHICLE_LOCATION in entry.runtime_data.scopes entities: list[BinarySensorEntity] = [] for vehicle in entry.runtime_data.vehicles: for description in VEHICLE_DESCRIPTIONS: + if description.requires_location_scope and not location_scope: + continue if ( not vehicle.poll and description.streaming_listener diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py index aea662982ccb..9b4de923337c 100644 --- a/homeassistant/components/teslemetry/sensor.py +++ b/homeassistant/components/teslemetry/sensor.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta from typing import override from tesla_fleet_api import firmware_at_least +from tesla_fleet_api.const import Scope from teslemetry_stream import TeslemetryStream, TeslemetryStreamVehicle from teslemetry_stream.const import CreditsEvent @@ -215,6 +216,7 @@ class TeslemetryVehicleSensorEntityDescription(SensorEntityDescription): ) = None streaming_firmware: str = "2024.26" requires_hw4: bool = False + requires_location_scope: bool = False VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = ( @@ -548,6 +550,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = ( ), TeslemetryVehicleSensorEntityDescription( key="drive_state_active_route_destination", + requires_location_scope=True, polling=True, streaming_listener=lambda vehicle, callback: vehicle.listen_DestinationName( callback @@ -1097,6 +1100,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = ( ), TeslemetryVehicleSensorEntityDescription( key="gps_heading", + requires_location_scope=True, streaming_listener=lambda vehicle, callback: vehicle.listen_GpsHeading( callback ), @@ -1674,9 +1678,12 @@ async def async_setup_entry( ) -> None: """Set up the Teslemetry sensor platform from a config entry.""" + location_scope = Scope.VEHICLE_LOCATION in entry.runtime_data.scopes entities: list[SensorEntity] = [] for vehicle in entry.runtime_data.vehicles: for description in VEHICLE_DESCRIPTIONS: + if description.requires_location_scope and not location_scope: + continue if ( not vehicle.poll and description.streaming_listener diff --git a/tests/components/teslemetry/test_binary_sensor.py b/tests/components/teslemetry/test_binary_sensor.py index 0033a4617ec8..8d02e3699914 100644 --- a/tests/components/teslemetry/test_binary_sensor.py +++ b/tests/components/teslemetry/test_binary_sensor.py @@ -7,13 +7,14 @@ import pytest from syrupy.assertion import SnapshotAssertion from teslemetry_stream import Signal +from homeassistant.components.teslemetry.const import DOMAIN from homeassistant.components.teslemetry.coordinator import VEHICLE_INTERVAL from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er from . import assert_entities, assert_entities_alt, setup_platform -from .const import VEHICLE_DATA_ALT +from .const import METADATA, VEHICLE_DATA_ALT from tests.common import async_fire_time_changed, mock_restore_cache @@ -162,3 +163,35 @@ async def test_binary_sensors_connectivity( # Assert the entities have correct state with concrete assertions assert hass.states.get("binary_sensor.test_cellular").state == "on" assert hass.states.get("binary_sensor.test_wi_fi").state == "off" + + +@pytest.mark.parametrize( + ("scopes", "expected"), + [ + pytest.param(METADATA["scopes"], True, id="location_scope"), + pytest.param( + [scope for scope in METADATA["scopes"] if scope != "vehicle_location"], + False, + id="no_location_scope", + ), + ], +) +async def test_gps_state_requires_location_scope( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_metadata: AsyncMock, + scopes: list[str], + expected: bool, +) -> None: + """Test the GPS state binary sensor is only created with the location scope.""" + + mock_metadata.return_value = {**METADATA, "scopes": scopes} + + await setup_platform(hass, [Platform.BINARY_SENSOR]) + + assert ( + entity_registry.async_get_entity_id( + Platform.BINARY_SENSOR, DOMAIN, "LRW3F7EK4NC700000-gps_state" + ) + is not None + ) is expected diff --git a/tests/components/teslemetry/test_sensor.py b/tests/components/teslemetry/test_sensor.py index dedd92874135..4eb8206358e7 100644 --- a/tests/components/teslemetry/test_sensor.py +++ b/tests/components/teslemetry/test_sensor.py @@ -745,3 +745,36 @@ async def test_energy_history_unavailable_while_stream_disconnected( mock_energy_totals_stream.send(is_cache=True) await hass.async_block_till_done() assert hass.states.get(ENERGY_HISTORY_ENTITY).state == "0.036" + + +@pytest.mark.parametrize( + ("scopes", "expected"), + [ + pytest.param(METADATA["scopes"], True, id="location_scope"), + pytest.param( + [scope for scope in METADATA["scopes"] if scope != "vehicle_location"], + False, + id="no_location_scope", + ), + ], +) +async def test_location_sensors_require_location_scope( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_metadata: AsyncMock, + scopes: list[str], + expected: bool, +) -> None: + """Test location sensors are only created with the vehicle location scope.""" + + mock_metadata.return_value = {**METADATA, "scopes": scopes} + + await setup_platform(hass, [Platform.SENSOR]) + + for key in ("gps_heading", "drive_state_active_route_destination"): + assert ( + entity_registry.async_get_entity_id( + Platform.SENSOR, DOMAIN, f"{VEHICLE_VIN}-{key}" + ) + is not None + ) is expected