1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-18 07:56:03 +01:00

Migrate Tessie setup and coordinator to tesla_fleet_api (#167018)

This commit is contained in:
Brett Adams
2026-04-01 21:53:08 +10:00
committed by GitHub
parent f2001db68c
commit 0b67644b97
4 changed files with 98 additions and 66 deletions

View File

@@ -1,19 +1,21 @@
"""Tessie integration.""" """Tessie integration."""
import asyncio import asyncio
from http import HTTPStatus
import logging import logging
from aiohttp import ClientError, ClientResponseError
from tesla_fleet_api.const import Scope from tesla_fleet_api.const import Scope
from tesla_fleet_api.exceptions import ( from tesla_fleet_api.exceptions import (
Forbidden, Forbidden,
GatewayTimeout,
InvalidResponse,
InvalidToken, InvalidToken,
MissingToken,
RateLimited,
ServiceUnavailable,
SubscriptionRequired, SubscriptionRequired,
TeslaFleetError, TeslaFleetError,
) )
from tesla_fleet_api.tessie import Tessie from tesla_fleet_api.tessie import Tessie
from tessie_api import get_state_of_all_vehicles
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, Platform from homeassistant.const import CONF_ACCESS_TOKEN, Platform
@@ -54,57 +56,69 @@ _LOGGER = logging.getLogger(__name__)
type TessieConfigEntry = ConfigEntry[TessieData] type TessieConfigEntry = ConfigEntry[TessieData]
RETRY_EXCEPTIONS = (
InvalidResponse,
RateLimited,
ServiceUnavailable,
GatewayTimeout,
)
async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bool:
"""Set up Tessie config.""" """Set up Tessie config."""
api_key = entry.data[CONF_ACCESS_TOKEN] api_key = entry.data[CONF_ACCESS_TOKEN]
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
tessie = Tessie(session, api_key)
try: try:
state_of_all_vehicles = await get_state_of_all_vehicles( state_of_all_vehicles = await tessie.list_vehicles(only_active=True)
session=session, except (InvalidToken, MissingToken) as e:
api_key=api_key, raise ConfigEntryAuthFailed from e
only_active=True, except RETRY_EXCEPTIONS as e:
)
except ClientResponseError as e:
if e.status == HTTPStatus.UNAUTHORIZED:
raise ConfigEntryAuthFailed from e
raise ConfigEntryError("Setup failed, unable to connect to Tessie") from e
except ClientError as e:
raise ConfigEntryNotReady from e raise ConfigEntryNotReady from e
except TeslaFleetError as e:
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from e
vehicles = [ vehicles: list[TessieVehicleData] = []
TessieVehicleData( for vehicle in state_of_all_vehicles["results"]:
vin=vehicle["vin"], if vehicle["last_state"] is None:
data_coordinator=TessieStateUpdateCoordinator( continue
hass,
entry, vin = vehicle["vin"]
api_key=api_key, vehicle_api = tessie.vehicles.create(vin)
vin=vehicle["vin"], vehicles.append(
data=vehicle["last_state"], TessieVehicleData(
), vin=vin,
device=DeviceInfo( data_coordinator=TessieStateUpdateCoordinator(
identifiers={(DOMAIN, vehicle["vin"])}, hass,
manufacturer="Tesla", entry,
configuration_url="https://my.tessie.com/", api=vehicle_api,
name=vehicle["last_state"]["display_name"], api_key=api_key,
model=MODELS.get( vin=vin,
vehicle["last_state"]["vehicle_config"]["car_type"], data=vehicle["last_state"],
vehicle["last_state"]["vehicle_config"]["car_type"],
), ),
sw_version=vehicle["last_state"]["vehicle_state"]["car_version"].split( device=DeviceInfo(
" " identifiers={(DOMAIN, vin)},
)[0], manufacturer="Tesla",
hw_version=vehicle["last_state"]["vehicle_config"]["driver_assist"], configuration_url="https://my.tessie.com/",
serial_number=vehicle["vin"], name=vehicle["last_state"]["display_name"],
), model=MODELS.get(
vehicle["last_state"]["vehicle_config"]["car_type"],
vehicle["last_state"]["vehicle_config"]["car_type"],
),
sw_version=vehicle["last_state"]["vehicle_state"][
"car_version"
].split(" ")[0],
hw_version=vehicle["last_state"]["vehicle_config"]["driver_assist"],
serial_number=vin,
),
)
) )
for vehicle in state_of_all_vehicles["results"]
if vehicle["last_state"] is not None
]
# Energy Sites # Energy Sites
tessie = Tessie(session, api_key)
energysites: list[TessieEnergyData] = [] energysites: list[TessieEnergyData] = []
try: try:

View File

@@ -10,8 +10,7 @@ from typing import TYPE_CHECKING, Any
from aiohttp import ClientError, ClientResponseError from aiohttp import ClientError, ClientResponseError
from tesla_fleet_api.const import TeslaEnergyPeriod from tesla_fleet_api.const import TeslaEnergyPeriod
from tesla_fleet_api.exceptions import InvalidToken, MissingToken, TeslaFleetError from tesla_fleet_api.exceptions import InvalidToken, MissingToken, TeslaFleetError
from tesla_fleet_api.tessie import EnergySite from tesla_fleet_api.tessie import EnergySite, Vehicle
from tessie_api import get_state
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
@@ -54,6 +53,7 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self, self,
hass: HomeAssistant, hass: HomeAssistant,
config_entry: TessieConfigEntry, config_entry: TessieConfigEntry,
api: Vehicle,
api_key: str, api_key: str,
vin: str, vin: str,
data: dict[str, Any], data: dict[str, Any],
@@ -66,6 +66,7 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
name="Tessie", name="Tessie",
update_interval=timedelta(seconds=TESSIE_SYNC_INTERVAL), update_interval=timedelta(seconds=TESSIE_SYNC_INTERVAL),
) )
self.api = api
self.api_key = api_key self.api_key = api_key
self.vin = vin self.vin = vin
self.session = async_get_clientsession(hass) self.session = async_get_clientsession(hass)
@@ -74,12 +75,14 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
async def _async_update_data(self) -> dict[str, Any]: async def _async_update_data(self) -> dict[str, Any]:
"""Update vehicle data using Tessie API.""" """Update vehicle data using Tessie API."""
try: try:
vehicle = await get_state( vehicle = await self.api.state(use_cache=True)
session=self.session, except (InvalidToken, MissingToken) as e:
api_key=self.api_key, raise ConfigEntryAuthFailed from e
vin=self.vin, except TeslaFleetError as e:
use_cache=True, raise UpdateFailed(
) translation_domain=DOMAIN,
translation_key="cannot_connect",
) from e
except ClientResponseError as e: except ClientResponseError as e:
if e.status == HTTPStatus.UNAUTHORIZED: if e.status == HTTPStatus.UNAUTHORIZED:
raise ConfigEntryAuthFailed from e raise ConfigEntryAuthFailed from e

View File

@@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from copy import deepcopy from copy import deepcopy
from unittest.mock import patch from unittest.mock import AsyncMock, patch
import pytest import pytest
@@ -25,9 +25,10 @@ from .common import (
def mock_get_state(): def mock_get_state():
"""Mock get_state function.""" """Mock get_state function."""
with patch( with patch(
"homeassistant.components.tessie.coordinator.get_state", "tesla_fleet_api.tessie.Vehicle.state",
return_value=TEST_VEHICLE_STATE_ONLINE, new_callable=AsyncMock,
) as mock_get_state: ) as mock_get_state:
mock_get_state.return_value = TEST_VEHICLE_STATE_ONLINE
yield mock_get_state yield mock_get_state
@@ -35,9 +36,10 @@ def mock_get_state():
def mock_get_state_of_all_vehicles(): def mock_get_state_of_all_vehicles():
"""Mock get_state_of_all_vehicles function.""" """Mock get_state_of_all_vehicles function."""
with patch( with patch(
"homeassistant.components.tessie.get_state_of_all_vehicles", "tesla_fleet_api.tessie.Tessie.list_vehicles",
return_value=TEST_STATE_OF_ALL_VEHICLES, new_callable=AsyncMock,
) as mock_get_state_of_all_vehicles: ) as mock_get_state_of_all_vehicles:
mock_get_state_of_all_vehicles.return_value = TEST_STATE_OF_ALL_VEHICLES
yield mock_get_state_of_all_vehicles yield mock_get_state_of_all_vehicles

View File

@@ -1,13 +1,18 @@
"""Test the Tessie init.""" """Test the Tessie init."""
from unittest.mock import patch from unittest.mock import AsyncMock, patch
from tesla_fleet_api.exceptions import TeslaFleetError from tesla_fleet_api.exceptions import (
InvalidRequest,
InvalidToken,
ServiceUnavailable,
TeslaFleetError,
)
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .common import ERROR_AUTH, ERROR_CONNECTION, ERROR_UNKNOWN, setup_platform from .common import setup_platform
async def test_load_unload(hass: HomeAssistant) -> None: async def test_load_unload(hass: HomeAssistant) -> None:
@@ -20,32 +25,40 @@ async def test_load_unload(hass: HomeAssistant) -> None:
assert entry.state is ConfigEntryState.NOT_LOADED assert entry.state is ConfigEntryState.NOT_LOADED
async def test_runtime_vehicle_api_handle_is_optional(hass: HomeAssistant) -> None:
"""Test the runtime vehicle API handle remains optional during migration."""
entry = await setup_platform(hass)
assert all(vehicle.api is None for vehicle in entry.runtime_data.vehicles)
async def test_auth_failure( async def test_auth_failure(
hass: HomeAssistant, mock_get_state_of_all_vehicles hass: HomeAssistant, mock_get_state_of_all_vehicles: AsyncMock
) -> None: ) -> None:
"""Test init with an authentication error.""" """Test init with an authentication error."""
mock_get_state_of_all_vehicles.side_effect = ERROR_AUTH mock_get_state_of_all_vehicles.side_effect = InvalidToken()
entry = await setup_platform(hass) entry = await setup_platform(hass)
assert entry.state is ConfigEntryState.SETUP_ERROR assert entry.state is ConfigEntryState.SETUP_ERROR
async def test_unknown_failure( async def test_unknown_failure(
hass: HomeAssistant, mock_get_state_of_all_vehicles hass: HomeAssistant, mock_get_state_of_all_vehicles: AsyncMock
) -> None: ) -> None:
"""Test init with an client response error.""" """Test init with a non-retryable fleet API error."""
mock_get_state_of_all_vehicles.side_effect = ERROR_UNKNOWN mock_get_state_of_all_vehicles.side_effect = InvalidRequest()
entry = await setup_platform(hass) entry = await setup_platform(hass)
assert entry.state is ConfigEntryState.SETUP_ERROR assert entry.state is ConfigEntryState.SETUP_ERROR
assert entry.reason == "Failed to connect"
async def test_connection_failure( async def test_retryable_api_failure(
hass: HomeAssistant, mock_get_state_of_all_vehicles hass: HomeAssistant, mock_get_state_of_all_vehicles: AsyncMock
) -> None: ) -> None:
"""Test init with a network connection error.""" """Test init with a retryable fleet API error."""
mock_get_state_of_all_vehicles.side_effect = ERROR_CONNECTION mock_get_state_of_all_vehicles.side_effect = ServiceUnavailable()
entry = await setup_platform(hass) entry = await setup_platform(hass)
assert entry.state is ConfigEntryState.SETUP_RETRY assert entry.state is ConfigEntryState.SETUP_RETRY