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:
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user