1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-02 00:20:30 +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."""
import asyncio
from http import HTTPStatus
import logging
from aiohttp import ClientError, ClientResponseError
from tesla_fleet_api.const import Scope
from tesla_fleet_api.exceptions import (
Forbidden,
GatewayTimeout,
InvalidResponse,
InvalidToken,
MissingToken,
RateLimited,
ServiceUnavailable,
SubscriptionRequired,
TeslaFleetError,
)
from tesla_fleet_api.tessie import Tessie
from tessie_api import get_state_of_all_vehicles
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
@@ -54,57 +56,69 @@ _LOGGER = logging.getLogger(__name__)
type TessieConfigEntry = ConfigEntry[TessieData]
RETRY_EXCEPTIONS = (
InvalidResponse,
RateLimited,
ServiceUnavailable,
GatewayTimeout,
)
async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bool:
"""Set up Tessie config."""
api_key = entry.data[CONF_ACCESS_TOKEN]
session = async_get_clientsession(hass)
tessie = Tessie(session, api_key)
try:
state_of_all_vehicles = await get_state_of_all_vehicles(
session=session,
api_key=api_key,
only_active=True,
)
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:
state_of_all_vehicles = await tessie.list_vehicles(only_active=True)
except (InvalidToken, MissingToken) as e:
raise ConfigEntryAuthFailed from e
except RETRY_EXCEPTIONS as e:
raise ConfigEntryNotReady from e
except TeslaFleetError as e:
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from e
vehicles = [
TessieVehicleData(
vin=vehicle["vin"],
data_coordinator=TessieStateUpdateCoordinator(
hass,
entry,
api_key=api_key,
vin=vehicle["vin"],
data=vehicle["last_state"],
),
device=DeviceInfo(
identifiers={(DOMAIN, vehicle["vin"])},
manufacturer="Tesla",
configuration_url="https://my.tessie.com/",
name=vehicle["last_state"]["display_name"],
model=MODELS.get(
vehicle["last_state"]["vehicle_config"]["car_type"],
vehicle["last_state"]["vehicle_config"]["car_type"],
vehicles: list[TessieVehicleData] = []
for vehicle in state_of_all_vehicles["results"]:
if vehicle["last_state"] is None:
continue
vin = vehicle["vin"]
vehicle_api = tessie.vehicles.create(vin)
vehicles.append(
TessieVehicleData(
vin=vin,
data_coordinator=TessieStateUpdateCoordinator(
hass,
entry,
api=vehicle_api,
api_key=api_key,
vin=vin,
data=vehicle["last_state"],
),
sw_version=vehicle["last_state"]["vehicle_state"]["car_version"].split(
" "
)[0],
hw_version=vehicle["last_state"]["vehicle_config"]["driver_assist"],
serial_number=vehicle["vin"],
),
device=DeviceInfo(
identifiers={(DOMAIN, vin)},
manufacturer="Tesla",
configuration_url="https://my.tessie.com/",
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
tessie = Tessie(session, api_key)
energysites: list[TessieEnergyData] = []
try:

View File

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

View File

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

View File

@@ -1,13 +1,18 @@
"""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.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:
@@ -20,32 +25,40 @@ async def test_load_unload(hass: HomeAssistant) -> None:
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(
hass: HomeAssistant, mock_get_state_of_all_vehicles
hass: HomeAssistant, mock_get_state_of_all_vehicles: AsyncMock
) -> None:
"""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)
assert entry.state is ConfigEntryState.SETUP_ERROR
async def test_unknown_failure(
hass: HomeAssistant, mock_get_state_of_all_vehicles
hass: HomeAssistant, mock_get_state_of_all_vehicles: AsyncMock
) -> 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)
assert entry.state is ConfigEntryState.SETUP_ERROR
assert entry.reason == "Failed to connect"
async def test_connection_failure(
hass: HomeAssistant, mock_get_state_of_all_vehicles
async def test_retryable_api_failure(
hass: HomeAssistant, mock_get_state_of_all_vehicles: AsyncMock
) -> 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)
assert entry.state is ConfigEntryState.SETUP_RETRY