Update vehicle type handling in Teslemetry (#148862)

This commit is contained in:
Brett Adams 2025-07-16 18:05:17 +10:00 committed by GitHub
parent bafd342d5d
commit 84e3dac406
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 105 additions and 2390 deletions

View File

@ -133,7 +133,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
) )
firmware = vehicle_metadata[vin].get("firmware", "Unknown") firmware = vehicle_metadata[vin].get("firmware", "Unknown")
stream_vehicle = stream.get_vehicle(vin) stream_vehicle = stream.get_vehicle(vin)
poll = product["command_signing"] == "off" poll = vehicle_metadata[vin].get("polling", False)
vehicles.append( vehicles.append(
TeslemetryVehicleData( TeslemetryVehicleData(

View File

@ -542,7 +542,7 @@ async def async_setup_entry(
for vehicle in entry.runtime_data.vehicles: for vehicle in entry.runtime_data.vehicles:
for description in VEHICLE_DESCRIPTIONS: for description in VEHICLE_DESCRIPTIONS:
if ( if (
not vehicle.api.pre2021 not vehicle.poll
and description.streaming_listener and description.streaming_listener
and vehicle.firmware >= description.streaming_firmware and vehicle.firmware >= description.streaming_firmware
): ):

View File

@ -67,7 +67,7 @@ async def async_setup_entry(
TeslemetryVehiclePollingClimateEntity( TeslemetryVehiclePollingClimateEntity(
vehicle, TeslemetryClimateSide.DRIVER, entry.runtime_data.scopes vehicle, TeslemetryClimateSide.DRIVER, entry.runtime_data.scopes
) )
if vehicle.api.pre2021 or vehicle.firmware < "2024.44.25" if vehicle.poll or vehicle.firmware < "2024.44.25"
else TeslemetryStreamingClimateEntity( else TeslemetryStreamingClimateEntity(
vehicle, TeslemetryClimateSide.DRIVER, entry.runtime_data.scopes vehicle, TeslemetryClimateSide.DRIVER, entry.runtime_data.scopes
) )
@ -77,7 +77,7 @@ async def async_setup_entry(
TeslemetryVehiclePollingCabinOverheatProtectionEntity( TeslemetryVehiclePollingCabinOverheatProtectionEntity(
vehicle, entry.runtime_data.scopes vehicle, entry.runtime_data.scopes
) )
if vehicle.api.pre2021 or vehicle.firmware < "2024.44.25" if vehicle.poll or vehicle.firmware < "2024.44.25"
else TeslemetryStreamingCabinOverheatProtectionEntity( else TeslemetryStreamingCabinOverheatProtectionEntity(
vehicle, entry.runtime_data.scopes vehicle, entry.runtime_data.scopes
) )

View File

@ -45,7 +45,7 @@ async def async_setup_entry(
chain( chain(
( (
TeslemetryVehiclePollingWindowEntity(vehicle, entry.runtime_data.scopes) TeslemetryVehiclePollingWindowEntity(vehicle, entry.runtime_data.scopes)
if vehicle.api.pre2021 or vehicle.firmware < "2024.26" if vehicle.poll or vehicle.firmware < "2024.26"
else TeslemetryStreamingWindowEntity(vehicle, entry.runtime_data.scopes) else TeslemetryStreamingWindowEntity(vehicle, entry.runtime_data.scopes)
for vehicle in entry.runtime_data.vehicles for vehicle in entry.runtime_data.vehicles
), ),
@ -53,7 +53,7 @@ async def async_setup_entry(
TeslemetryVehiclePollingChargePortEntity( TeslemetryVehiclePollingChargePortEntity(
vehicle, entry.runtime_data.scopes vehicle, entry.runtime_data.scopes
) )
if vehicle.api.pre2021 or vehicle.firmware < "2024.44.25" if vehicle.poll or vehicle.firmware < "2024.44.25"
else TeslemetryStreamingChargePortEntity( else TeslemetryStreamingChargePortEntity(
vehicle, entry.runtime_data.scopes vehicle, entry.runtime_data.scopes
) )
@ -63,7 +63,7 @@ async def async_setup_entry(
TeslemetryVehiclePollingFrontTrunkEntity( TeslemetryVehiclePollingFrontTrunkEntity(
vehicle, entry.runtime_data.scopes vehicle, entry.runtime_data.scopes
) )
if vehicle.api.pre2021 or vehicle.firmware < "2024.26" if vehicle.poll or vehicle.firmware < "2024.26"
else TeslemetryStreamingFrontTrunkEntity( else TeslemetryStreamingFrontTrunkEntity(
vehicle, entry.runtime_data.scopes vehicle, entry.runtime_data.scopes
) )
@ -73,7 +73,7 @@ async def async_setup_entry(
TeslemetryVehiclePollingRearTrunkEntity( TeslemetryVehiclePollingRearTrunkEntity(
vehicle, entry.runtime_data.scopes vehicle, entry.runtime_data.scopes
) )
if vehicle.api.pre2021 or vehicle.firmware < "2024.26" if vehicle.poll or vehicle.firmware < "2024.26"
else TeslemetryStreamingRearTrunkEntity( else TeslemetryStreamingRearTrunkEntity(
vehicle, entry.runtime_data.scopes vehicle, entry.runtime_data.scopes
) )
@ -82,7 +82,8 @@ async def async_setup_entry(
( (
TeslemetrySunroofEntity(vehicle, entry.runtime_data.scopes) TeslemetrySunroofEntity(vehicle, entry.runtime_data.scopes)
for vehicle in entry.runtime_data.vehicles for vehicle in entry.runtime_data.vehicles
if vehicle.coordinator.data.get("vehicle_config_sun_roof_installed") if vehicle.poll
and vehicle.coordinator.data.get("vehicle_config_sun_roof_installed")
), ),
) )
) )

View File

@ -89,7 +89,7 @@ async def async_setup_entry(
for vehicle in entry.runtime_data.vehicles: for vehicle in entry.runtime_data.vehicles:
for description in DESCRIPTIONS: for description in DESCRIPTIONS:
if vehicle.api.pre2021 or vehicle.firmware < description.streaming_firmware: if vehicle.poll or vehicle.firmware < description.streaming_firmware:
if description.polling_prefix: if description.polling_prefix:
entities.append( entities.append(
TeslemetryVehiclePollingDeviceTrackerEntity( TeslemetryVehiclePollingDeviceTrackerEntity(

View File

@ -42,7 +42,7 @@ async def async_setup_entry(
TeslemetryVehiclePollingVehicleLockEntity( TeslemetryVehiclePollingVehicleLockEntity(
vehicle, Scope.VEHICLE_CMDS in entry.runtime_data.scopes vehicle, Scope.VEHICLE_CMDS in entry.runtime_data.scopes
) )
if vehicle.api.pre2021 or vehicle.firmware < "2024.26" if vehicle.poll or vehicle.firmware < "2024.26"
else TeslemetryStreamingVehicleLockEntity( else TeslemetryStreamingVehicleLockEntity(
vehicle, Scope.VEHICLE_CMDS in entry.runtime_data.scopes vehicle, Scope.VEHICLE_CMDS in entry.runtime_data.scopes
) )
@ -52,7 +52,7 @@ async def async_setup_entry(
TeslemetryVehiclePollingCableLockEntity( TeslemetryVehiclePollingCableLockEntity(
vehicle, Scope.VEHICLE_CMDS in entry.runtime_data.scopes vehicle, Scope.VEHICLE_CMDS in entry.runtime_data.scopes
) )
if vehicle.api.pre2021 or vehicle.firmware < "2024.26" if vehicle.poll or vehicle.firmware < "2024.26"
else TeslemetryStreamingCableLockEntity( else TeslemetryStreamingCableLockEntity(
vehicle, Scope.VEHICLE_CMDS in entry.runtime_data.scopes vehicle, Scope.VEHICLE_CMDS in entry.runtime_data.scopes
) )

View File

@ -53,7 +53,7 @@ async def async_setup_entry(
async_add_entities( async_add_entities(
TeslemetryVehiclePollingMediaEntity(vehicle, entry.runtime_data.scopes) TeslemetryVehiclePollingMediaEntity(vehicle, entry.runtime_data.scopes)
if vehicle.api.pre2021 or vehicle.firmware < "2025.2.6" if vehicle.poll or vehicle.firmware < "2025.2.6"
else TeslemetryStreamingMediaEntity(vehicle, entry.runtime_data.scopes) else TeslemetryStreamingMediaEntity(vehicle, entry.runtime_data.scopes)
for vehicle in entry.runtime_data.vehicles for vehicle in entry.runtime_data.vehicles
) )

View File

@ -145,7 +145,7 @@ async def async_setup_entry(
description, description,
entry.runtime_data.scopes, entry.runtime_data.scopes,
) )
if vehicle.api.pre2021 or vehicle.firmware < "2024.26" if vehicle.poll or vehicle.firmware < "2024.26"
else TeslemetryStreamingNumberEntity( else TeslemetryStreamingNumberEntity(
vehicle, vehicle,
description, description,

View File

@ -180,7 +180,7 @@ async def async_setup_entry(
TeslemetryVehiclePollingSelectEntity( TeslemetryVehiclePollingSelectEntity(
vehicle, description, entry.runtime_data.scopes vehicle, description, entry.runtime_data.scopes
) )
if vehicle.api.pre2021 if vehicle.poll
or vehicle.firmware < "2024.26" or vehicle.firmware < "2024.26"
or description.streaming_listener is None or description.streaming_listener is None
else TeslemetryStreamingSelectEntity( else TeslemetryStreamingSelectEntity(

View File

@ -1565,7 +1565,7 @@ async def async_setup_entry(
for vehicle in entry.runtime_data.vehicles: for vehicle in entry.runtime_data.vehicles:
for description in VEHICLE_DESCRIPTIONS: for description in VEHICLE_DESCRIPTIONS:
if ( if (
not vehicle.api.pre2021 not vehicle.poll
and description.streaming_listener and description.streaming_listener
and vehicle.firmware >= description.streaming_firmware and vehicle.firmware >= description.streaming_firmware
): ):
@ -1575,7 +1575,7 @@ async def async_setup_entry(
for time_description in VEHICLE_TIME_DESCRIPTIONS: for time_description in VEHICLE_TIME_DESCRIPTIONS:
if ( if (
not vehicle.api.pre2021 not vehicle.poll
and vehicle.firmware >= time_description.streaming_firmware and vehicle.firmware >= time_description.streaming_firmware
): ):
entities.append( entities.append(

View File

@ -147,8 +147,7 @@ async def async_setup_entry(
TeslemetryVehiclePollingVehicleSwitchEntity( TeslemetryVehiclePollingVehicleSwitchEntity(
vehicle, description, entry.runtime_data.scopes vehicle, description, entry.runtime_data.scopes
) )
if vehicle.api.pre2021 if vehicle.poll or vehicle.firmware < description.streaming_firmware
or vehicle.firmware < description.streaming_firmware
else TeslemetryStreamingVehicleSwitchEntity( else TeslemetryStreamingVehicleSwitchEntity(
vehicle, description, entry.runtime_data.scopes vehicle, description, entry.runtime_data.scopes
) )

View File

@ -39,7 +39,7 @@ async def async_setup_entry(
async_add_entities( async_add_entities(
TeslemetryVehiclePollingUpdateEntity(vehicle, entry.runtime_data.scopes) TeslemetryVehiclePollingUpdateEntity(vehicle, entry.runtime_data.scopes)
if vehicle.api.pre2021 or vehicle.firmware < "2024.44.25" if vehicle.poll or vehicle.firmware < "2024.44.25"
else TeslemetryStreamingUpdateEntity(vehicle, entry.runtime_data.scopes) else TeslemetryStreamingUpdateEntity(vehicle, entry.runtime_data.scopes)
for vehicle in entry.runtime_data.vehicles for vehicle in entry.runtime_data.vehicles
) )

View File

@ -14,6 +14,7 @@ from .const import (
ENERGY_HISTORY, ENERGY_HISTORY,
LIVE_STATUS, LIVE_STATUS,
METADATA, METADATA,
METADATA_LEGACY,
PRODUCTS, PRODUCTS,
SITE_INFO, SITE_INFO,
VEHICLE_DATA, VEHICLE_DATA,
@ -53,9 +54,9 @@ def mock_vehicle_data() -> Generator[AsyncMock]:
def mock_legacy(): def mock_legacy():
"""Mock Tesla Fleet Api products method.""" """Mock Tesla Fleet Api products method."""
with patch( with patch(
"tesla_fleet_api.teslemetry.Vehicle.pre2021", return_value=True "tesla_fleet_api.teslemetry.Teslemetry.metadata", return_value=METADATA_LEGACY
) as mock_pre2021: ) as mock_products:
yield mock_pre2021 yield mock_products
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)

View File

@ -37,6 +37,32 @@ COMMAND_ERRORS = (COMMAND_REASON, COMMAND_NOREASON, COMMAND_ERROR, COMMAND_NOERR
RESPONSE_OK = {"response": {}, "error": None} RESPONSE_OK = {"response": {}, "error": None}
METADATA = { METADATA = {
"uid": "abc-123",
"region": "NA",
"scopes": [
"openid",
"offline_access",
"user_data",
"vehicle_device_data",
"vehicle_cmds",
"vehicle_charging_cmds",
"vehicle_location",
"energy_device_data",
"energy_cmds",
],
"vehicles": {
"LRW3F7EK4NC700000": {
"proxy": True,
"access": True,
"polling": False,
"firmware": "2026.0.0",
"discounted": False,
"fleet_telemetry": "1.0.2",
"name": "Home Assistant",
}
},
}
METADATA_LEGACY = {
"uid": "abc-123", "uid": "abc-123",
"region": "NA", "region": "NA",
"scopes": [ "scopes": [
@ -56,6 +82,9 @@ METADATA = {
"access": True, "access": True,
"polling": True, "polling": True,
"firmware": "2026.0.0", "firmware": "2026.0.0",
"discounted": True,
"fleet_telemetry": "unknown",
"name": "Home Assistant",
} }
}, },
} }
@ -68,7 +97,10 @@ METADATA_NOSCOPE = {
"proxy": False, "proxy": False,
"access": True, "access": True,
"polling": True, "polling": True,
"firmware": "2024.44.25", "firmware": "2026.0.0",
"discounted": True,
"fleet_telemetry": "unknown",
"name": "Home Assistant",
} }
}, },
} }

File diff suppressed because it is too large Load Diff

View File

@ -407,9 +407,8 @@
]), ]),
'max_temp': 40, 'max_temp': 40,
'min_temp': 30, 'min_temp': 30,
'supported_features': <ClimateEntityFeature: 385>, 'supported_features': <ClimateEntityFeature: 384>,
'target_temp_step': 5, 'target_temp_step': 5,
'temperature': None,
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'climate.test_cabin_overheat_protection', 'entity_id': 'climate.test_cabin_overheat_protection',

View File

@ -23,6 +23,7 @@ async def test_binary_sensor(
hass: HomeAssistant, hass: HomeAssistant,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
mock_legacy: AsyncMock,
) -> None: ) -> None:
"""Tests that the binary sensor entities are correct.""" """Tests that the binary sensor entities are correct."""
@ -37,6 +38,7 @@ async def test_binary_sensor_refresh(
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
mock_vehicle_data: AsyncMock, mock_vehicle_data: AsyncMock,
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,
mock_legacy: AsyncMock,
) -> None: ) -> None:
"""Tests that the binary sensor entities are correct.""" """Tests that the binary sensor entities are correct."""

View File

@ -273,7 +273,6 @@ async def test_climate_noscope(
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
mock_metadata: AsyncMock, mock_metadata: AsyncMock,
mock_legacy: AsyncMock,
) -> None: ) -> None:
"""Tests that the climate entity is correct.""" """Tests that the climate entity is correct."""
mock_metadata.return_value = METADATA_NOSCOPE mock_metadata.return_value = METADATA_NOSCOPE

View File

@ -55,7 +55,6 @@ async def test_cover_noscope(
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
mock_metadata: AsyncMock, mock_metadata: AsyncMock,
mock_legacy: AsyncMock,
) -> None: ) -> None:
"""Tests that the cover entities are correct without scopes.""" """Tests that the cover entities are correct without scopes."""
@ -67,6 +66,7 @@ async def test_cover_noscope(
@pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_cover_services( async def test_cover_services(
hass: HomeAssistant, hass: HomeAssistant,
mock_legacy: AsyncMock,
) -> None: ) -> None:
"""Tests that the cover entities are correct.""" """Tests that the cover entities are correct."""

View File

@ -49,7 +49,6 @@ async def test_device_tracker_noscope(
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
mock_metadata: AsyncMock, mock_metadata: AsyncMock,
mock_vehicle_data: AsyncMock, mock_vehicle_data: AsyncMock,
mock_legacy: AsyncMock,
) -> None: ) -> None:
"""Tests that the device tracker entities are correct.""" """Tests that the device tracker entities are correct."""

View File

@ -1,5 +1,7 @@
"""Test the Telemetry Diagnostics.""" """Test the Telemetry Diagnostics."""
from unittest.mock import AsyncMock
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
@ -18,6 +20,7 @@ async def test_diagnostics(
hass_client: ClientSessionGenerator, hass_client: ClientSessionGenerator,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,
mock_legacy: AsyncMock,
) -> None: ) -> None:
"""Test diagnostics.""" """Test diagnostics."""

View File

@ -14,7 +14,13 @@ from tesla_fleet_api.exceptions import (
from homeassistant.components.teslemetry.coordinator import VEHICLE_INTERVAL from homeassistant.components.teslemetry.coordinator import VEHICLE_INTERVAL
from homeassistant.components.teslemetry.models import TeslemetryData from homeassistant.components.teslemetry.models import TeslemetryData
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN, Platform from homeassistant.const import (
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
Platform,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
@ -72,6 +78,7 @@ async def test_vehicle_refresh_error(
mock_vehicle_data: AsyncMock, mock_vehicle_data: AsyncMock,
side_effect: TeslaFleetError, side_effect: TeslaFleetError,
state: ConfigEntryState, state: ConfigEntryState,
mock_legacy: AsyncMock,
) -> None: ) -> None:
"""Test coordinator refresh with an error.""" """Test coordinator refresh with an error."""
mock_vehicle_data.side_effect = side_effect mock_vehicle_data.side_effect = side_effect
@ -107,6 +114,7 @@ async def test_energy_site_refresh_error(
assert entry.state is state assert entry.state is state
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_vehicle_stream( async def test_vehicle_stream(
hass: HomeAssistant, hass: HomeAssistant,
mock_add_listener: AsyncMock, mock_add_listener: AsyncMock,
@ -121,7 +129,7 @@ async def test_vehicle_stream(
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
state = hass.states.get("binary_sensor.test_user_present") state = hass.states.get("binary_sensor.test_user_present")
assert state.state == STATE_OFF assert state.state == STATE_UNAVAILABLE
mock_add_listener.send( mock_add_listener.send(
{ {

View File

@ -55,7 +55,6 @@ async def test_media_player_noscope(
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
mock_metadata: AsyncMock, mock_metadata: AsyncMock,
mock_legacy: AsyncMock,
) -> None: ) -> None:
"""Tests that the media player entities are correct without required scope.""" """Tests that the media player entities are correct without required scope."""

View File

@ -1,6 +1,6 @@
"""Test the Teslemetry sensor platform.""" """Test the Teslemetry sensor platform."""
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
@ -26,6 +26,7 @@ async def test_sensors(
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,
mock_vehicle_data: AsyncMock, mock_vehicle_data: AsyncMock,
mock_legacy: AsyncMock,
) -> None: ) -> None:
"""Tests that the sensor entities with the legacy polling are correct.""" """Tests that the sensor entities with the legacy polling are correct."""
@ -33,9 +34,7 @@ async def test_sensors(
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
# Force the vehicle to use polling entry = await setup_platform(hass, [Platform.SENSOR])
with patch("tesla_fleet_api.teslemetry.Vehicle.pre2021", return_value=True):
entry = await setup_platform(hass, [Platform.SENSOR])
assert_entities(hass, entry.entry_id, entity_registry, snapshot) assert_entities(hass, entry.entry_id, entity_registry, snapshot)