Add Tesla Fleet integration (#122019)
* Add Tesla Fleet * Remove debug * Improvements * Fix refresh and stage tests * Working oauth flow test * Config Flow tests * Fixes * fixes * Remove comment Co-authored-by: G Johansson <goran.johansson@shiftit.se> * Remove TYPE_CHECKING * Add more tests * More tests * More tests * revert teslemetry change --------- Co-authored-by: G Johansson <goran.johansson@shiftit.se>
This commit is contained in:
parent
474e8b7a43
commit
a2c2488c8b
31 changed files with 6887 additions and 1 deletions
|
@ -1432,6 +1432,8 @@ build.json @home-assistant/supervisor
|
||||||
/tests/components/tellduslive/ @fredrike
|
/tests/components/tellduslive/ @fredrike
|
||||||
/homeassistant/components/template/ @PhracturedBlue @tetienne @home-assistant/core
|
/homeassistant/components/template/ @PhracturedBlue @tetienne @home-assistant/core
|
||||||
/tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core
|
/tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core
|
||||||
|
/homeassistant/components/tesla_fleet/ @Bre77
|
||||||
|
/tests/components/tesla_fleet/ @Bre77
|
||||||
/homeassistant/components/tesla_wall_connector/ @einarhauks
|
/homeassistant/components/tesla_wall_connector/ @einarhauks
|
||||||
/tests/components/tesla_wall_connector/ @einarhauks
|
/tests/components/tesla_wall_connector/ @einarhauks
|
||||||
/homeassistant/components/teslemetry/ @Bre77
|
/homeassistant/components/teslemetry/ @Bre77
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"domain": "tesla",
|
"domain": "tesla",
|
||||||
"name": "Tesla",
|
"name": "Tesla",
|
||||||
"integrations": ["powerwall", "tesla_wall_connector"]
|
"integrations": ["powerwall", "tesla_wall_connector", "tesla_fleet"]
|
||||||
}
|
}
|
||||||
|
|
169
homeassistant/components/tesla_fleet/__init__.py
Normal file
169
homeassistant/components/tesla_fleet/__init__.py
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
"""Tesla Fleet integration."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
from tesla_fleet_api import EnergySpecific, TeslaFleetApi, VehicleSpecific
|
||||||
|
from tesla_fleet_api.const import Scope
|
||||||
|
from tesla_fleet_api.exceptions import (
|
||||||
|
InvalidToken,
|
||||||
|
LoginRequired,
|
||||||
|
OAuthExpired,
|
||||||
|
TeslaFleetError,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN, Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||||
|
OAuth2Session,
|
||||||
|
async_get_config_entry_implementation,
|
||||||
|
)
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
|
||||||
|
from .const import DOMAIN, MODELS
|
||||||
|
from .coordinator import (
|
||||||
|
TeslaFleetEnergySiteInfoCoordinator,
|
||||||
|
TeslaFleetEnergySiteLiveCoordinator,
|
||||||
|
TeslaFleetVehicleDataCoordinator,
|
||||||
|
)
|
||||||
|
from .models import TeslaFleetData, TeslaFleetEnergyData, TeslaFleetVehicleData
|
||||||
|
|
||||||
|
PLATFORMS: Final = [Platform.SENSOR]
|
||||||
|
|
||||||
|
type TeslaFleetConfigEntry = ConfigEntry[TeslaFleetData]
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) -> bool:
|
||||||
|
"""Set up TeslaFleet config."""
|
||||||
|
|
||||||
|
access_token = entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN]
|
||||||
|
session = async_get_clientsession(hass)
|
||||||
|
|
||||||
|
token = jwt.decode(access_token, options={"verify_signature": False})
|
||||||
|
scopes = token["scp"]
|
||||||
|
region = token["ou_code"].lower()
|
||||||
|
|
||||||
|
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||||
|
oauth_session = OAuth2Session(hass, entry, implementation)
|
||||||
|
refresh_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
async def _refresh_token() -> str:
|
||||||
|
async with refresh_lock:
|
||||||
|
await oauth_session.async_ensure_token_valid()
|
||||||
|
token: str = oauth_session.token[CONF_ACCESS_TOKEN]
|
||||||
|
return token
|
||||||
|
|
||||||
|
# Create API connection
|
||||||
|
tesla = TeslaFleetApi(
|
||||||
|
session=session,
|
||||||
|
access_token=access_token,
|
||||||
|
region=region,
|
||||||
|
charging_scope=False,
|
||||||
|
partner_scope=False,
|
||||||
|
user_scope=False,
|
||||||
|
energy_scope=Scope.ENERGY_DEVICE_DATA in scopes,
|
||||||
|
vehicle_scope=Scope.VEHICLE_DEVICE_DATA in scopes,
|
||||||
|
refresh_hook=_refresh_token,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
products = (await tesla.products())["response"]
|
||||||
|
except (InvalidToken, OAuthExpired, LoginRequired) as e:
|
||||||
|
raise ConfigEntryAuthFailed from e
|
||||||
|
except TeslaFleetError as e:
|
||||||
|
raise ConfigEntryNotReady from e
|
||||||
|
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
|
||||||
|
# Create array of classes
|
||||||
|
vehicles: list[TeslaFleetVehicleData] = []
|
||||||
|
energysites: list[TeslaFleetEnergyData] = []
|
||||||
|
for product in products:
|
||||||
|
if "vin" in product and tesla.vehicle:
|
||||||
|
# Remove the protobuff 'cached_data' that we do not use to save memory
|
||||||
|
product.pop("cached_data", None)
|
||||||
|
vin = product["vin"]
|
||||||
|
api = VehicleSpecific(tesla.vehicle, vin)
|
||||||
|
coordinator = TeslaFleetVehicleDataCoordinator(hass, api, product)
|
||||||
|
|
||||||
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
device = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, vin)},
|
||||||
|
manufacturer="Tesla",
|
||||||
|
name=product["display_name"],
|
||||||
|
model=MODELS.get(vin[3]),
|
||||||
|
serial_number=vin,
|
||||||
|
)
|
||||||
|
|
||||||
|
vehicles.append(
|
||||||
|
TeslaFleetVehicleData(
|
||||||
|
api=api,
|
||||||
|
coordinator=coordinator,
|
||||||
|
vin=vin,
|
||||||
|
device=device,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif "energy_site_id" in product and tesla.energy:
|
||||||
|
site_id = product["energy_site_id"]
|
||||||
|
api = EnergySpecific(tesla.energy, site_id)
|
||||||
|
|
||||||
|
live_coordinator = TeslaFleetEnergySiteLiveCoordinator(hass, api)
|
||||||
|
info_coordinator = TeslaFleetEnergySiteInfoCoordinator(hass, api, product)
|
||||||
|
|
||||||
|
await live_coordinator.async_config_entry_first_refresh()
|
||||||
|
await info_coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
# Create energy site model
|
||||||
|
model = None
|
||||||
|
models = set()
|
||||||
|
for gateway in info_coordinator.data.get("components_gateways", []):
|
||||||
|
if gateway.get("part_name"):
|
||||||
|
models.add(gateway["part_name"])
|
||||||
|
for battery in info_coordinator.data.get("components_batteries", []):
|
||||||
|
if battery.get("part_name"):
|
||||||
|
models.add(battery["part_name"])
|
||||||
|
if models:
|
||||||
|
model = ", ".join(sorted(models))
|
||||||
|
|
||||||
|
device = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, str(site_id))},
|
||||||
|
manufacturer="Tesla",
|
||||||
|
name=product.get("site_name", "Energy Site"),
|
||||||
|
model=model,
|
||||||
|
serial_number=str(site_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create the energy site device regardless of it having entities
|
||||||
|
# This is so users with a Wall Connector but without a Powerwall can still make service calls
|
||||||
|
device_registry.async_get_or_create(
|
||||||
|
config_entry_id=entry.entry_id, **device
|
||||||
|
)
|
||||||
|
|
||||||
|
energysites.append(
|
||||||
|
TeslaFleetEnergyData(
|
||||||
|
api=api,
|
||||||
|
live_coordinator=live_coordinator,
|
||||||
|
info_coordinator=info_coordinator,
|
||||||
|
id=site_id,
|
||||||
|
device=device,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Setup Platforms
|
||||||
|
entry.runtime_data = TeslaFleetData(vehicles, energysites, scopes)
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) -> bool:
|
||||||
|
"""Unload TeslaFleet Config."""
|
||||||
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
@ -0,0 +1,71 @@
|
||||||
|
"""Application Credentials platform the Tesla Fleet integration."""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import secrets
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.application_credentials import ClientCredential
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import config_entry_oauth2_flow
|
||||||
|
|
||||||
|
from .const import DOMAIN, SCOPES
|
||||||
|
|
||||||
|
CLIENT_ID = "71b813eb-4a2e-483a-b831-4dec5cb9bf0d"
|
||||||
|
AUTHORIZE_URL = "https://auth.tesla.com/oauth2/v3/authorize"
|
||||||
|
TOKEN_URL = "https://auth.tesla.com/oauth2/v3/token"
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_auth_implementation(
|
||||||
|
hass: HomeAssistant, auth_domain: str, credential: ClientCredential
|
||||||
|
) -> config_entry_oauth2_flow.AbstractOAuth2Implementation:
|
||||||
|
"""Return auth implementation."""
|
||||||
|
return TeslaOAuth2Implementation(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TeslaOAuth2Implementation(config_entry_oauth2_flow.LocalOAuth2Implementation):
|
||||||
|
"""Tesla Fleet API Open Source Oauth2 implementation."""
|
||||||
|
|
||||||
|
_name = "Tesla Fleet API"
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, domain: str) -> None:
|
||||||
|
"""Initialize local auth implementation."""
|
||||||
|
self.hass = hass
|
||||||
|
self._domain = domain
|
||||||
|
|
||||||
|
# Setup PKCE
|
||||||
|
self.code_verifier = secrets.token_urlsafe(32)
|
||||||
|
hashed_verifier = hashlib.sha256(self.code_verifier.encode()).digest()
|
||||||
|
self.code_challenge = (
|
||||||
|
base64.urlsafe_b64encode(hashed_verifier).decode().replace("=", "")
|
||||||
|
)
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
domain,
|
||||||
|
CLIENT_ID,
|
||||||
|
"", # Implementation has no client secret
|
||||||
|
AUTHORIZE_URL,
|
||||||
|
TOKEN_URL,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extra_authorize_data(self) -> dict[str, Any]:
|
||||||
|
"""Extra data that needs to be appended to the authorize url."""
|
||||||
|
return {
|
||||||
|
"scope": " ".join(SCOPES),
|
||||||
|
"code_challenge": self.code_challenge, # PKCE
|
||||||
|
}
|
||||||
|
|
||||||
|
async def async_resolve_external_data(self, external_data: Any) -> dict:
|
||||||
|
"""Resolve the authorization code to tokens."""
|
||||||
|
return await self._token_request(
|
||||||
|
{
|
||||||
|
"grant_type": "authorization_code",
|
||||||
|
"code": external_data["code"],
|
||||||
|
"redirect_uri": external_data["state"]["redirect_uri"],
|
||||||
|
"code_verifier": self.code_verifier, # PKCE
|
||||||
|
}
|
||||||
|
)
|
75
homeassistant/components/tesla_fleet/config_flow.py
Normal file
75
homeassistant/components/tesla_fleet/config_flow.py
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
"""Config Flow for Tesla Fleet integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
|
||||||
|
from homeassistant.helpers import config_entry_oauth2_flow
|
||||||
|
|
||||||
|
from .const import DOMAIN, LOGGER
|
||||||
|
|
||||||
|
|
||||||
|
class OAuth2FlowHandler(
|
||||||
|
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
|
||||||
|
):
|
||||||
|
"""Config flow to handle Tesla Fleet API OAuth2 authentication."""
|
||||||
|
|
||||||
|
DOMAIN = DOMAIN
|
||||||
|
reauth_entry: ConfigEntry | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def logger(self) -> logging.Logger:
|
||||||
|
"""Return logger."""
|
||||||
|
return LOGGER
|
||||||
|
|
||||||
|
async def async_oauth_create_entry(
|
||||||
|
self,
|
||||||
|
data: dict[str, Any],
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle the initial step."""
|
||||||
|
|
||||||
|
token = jwt.decode(
|
||||||
|
data["token"]["access_token"], options={"verify_signature": False}
|
||||||
|
)
|
||||||
|
uid = token["sub"]
|
||||||
|
|
||||||
|
if not self.reauth_entry:
|
||||||
|
await self.async_set_unique_id(uid)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
return self.async_create_entry(title=uid, data=data)
|
||||||
|
|
||||||
|
if self.reauth_entry.unique_id == uid:
|
||||||
|
self.hass.config_entries.async_update_entry(
|
||||||
|
self.reauth_entry,
|
||||||
|
data=data,
|
||||||
|
)
|
||||||
|
await self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
|
||||||
|
return self.async_abort(reason="reauth_successful")
|
||||||
|
|
||||||
|
return self.async_abort(
|
||||||
|
reason="reauth_account_mismatch",
|
||||||
|
description_placeholders={"title": self.reauth_entry.title},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_reauth(
|
||||||
|
self, entry_data: Mapping[str, Any]
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Perform reauth upon an API authentication error."""
|
||||||
|
self.reauth_entry = self.hass.config_entries.async_get_entry(
|
||||||
|
self.context["entry_id"]
|
||||||
|
)
|
||||||
|
return await self.async_step_reauth_confirm()
|
||||||
|
|
||||||
|
async def async_step_reauth_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Confirm reauth dialog."""
|
||||||
|
if user_input is None:
|
||||||
|
return self.async_show_form(step_id="reauth_confirm")
|
||||||
|
return await self.async_step_user()
|
39
homeassistant/components/tesla_fleet/const.py
Normal file
39
homeassistant/components/tesla_fleet/const.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
"""Constants used by Tesla Fleet integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import StrEnum
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from tesla_fleet_api.const import Scope
|
||||||
|
|
||||||
|
DOMAIN = "tesla_fleet"
|
||||||
|
|
||||||
|
CONF_REFRESH_TOKEN = "refresh_token"
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__package__)
|
||||||
|
|
||||||
|
SCOPES = [
|
||||||
|
Scope.OPENID,
|
||||||
|
Scope.OFFLINE_ACCESS,
|
||||||
|
Scope.VEHICLE_DEVICE_DATA,
|
||||||
|
Scope.VEHICLE_CMDS,
|
||||||
|
Scope.VEHICLE_CHARGING_CMDS,
|
||||||
|
Scope.ENERGY_DEVICE_DATA,
|
||||||
|
Scope.ENERGY_CMDS,
|
||||||
|
]
|
||||||
|
|
||||||
|
MODELS = {
|
||||||
|
"S": "Model S",
|
||||||
|
"3": "Model 3",
|
||||||
|
"X": "Model X",
|
||||||
|
"Y": "Model Y",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TeslaFleetState(StrEnum):
|
||||||
|
"""Teslemetry Vehicle States."""
|
||||||
|
|
||||||
|
ONLINE = "online"
|
||||||
|
ASLEEP = "asleep"
|
||||||
|
OFFLINE = "offline"
|
215
homeassistant/components/tesla_fleet/coordinator.py
Normal file
215
homeassistant/components/tesla_fleet/coordinator.py
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
"""Tesla Fleet Data Coordinator."""
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from tesla_fleet_api import EnergySpecific, VehicleSpecific
|
||||||
|
from tesla_fleet_api.const import VehicleDataEndpoint
|
||||||
|
from tesla_fleet_api.exceptions import (
|
||||||
|
InvalidToken,
|
||||||
|
LoginRequired,
|
||||||
|
OAuthExpired,
|
||||||
|
RateLimited,
|
||||||
|
TeslaFleetError,
|
||||||
|
VehicleOffline,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import LOGGER, TeslaFleetState
|
||||||
|
|
||||||
|
VEHICLE_INTERVAL_SECONDS = 120
|
||||||
|
VEHICLE_INTERVAL = timedelta(seconds=VEHICLE_INTERVAL_SECONDS)
|
||||||
|
VEHICLE_WAIT = timedelta(minutes=15)
|
||||||
|
|
||||||
|
ENERGY_INTERVAL_SECONDS = 60
|
||||||
|
ENERGY_INTERVAL = timedelta(seconds=ENERGY_INTERVAL_SECONDS)
|
||||||
|
|
||||||
|
ENDPOINTS = [
|
||||||
|
VehicleDataEndpoint.CHARGE_STATE,
|
||||||
|
VehicleDataEndpoint.CLIMATE_STATE,
|
||||||
|
VehicleDataEndpoint.DRIVE_STATE,
|
||||||
|
VehicleDataEndpoint.LOCATION_DATA,
|
||||||
|
VehicleDataEndpoint.VEHICLE_STATE,
|
||||||
|
VehicleDataEndpoint.VEHICLE_CONFIG,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def flatten(data: dict[str, Any], parent: str | None = None) -> dict[str, Any]:
|
||||||
|
"""Flatten the data structure."""
|
||||||
|
result = {}
|
||||||
|
for key, value in data.items():
|
||||||
|
if parent:
|
||||||
|
key = f"{parent}_{key}"
|
||||||
|
if isinstance(value, dict):
|
||||||
|
result.update(flatten(value, key))
|
||||||
|
else:
|
||||||
|
result[key] = value
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class TeslaFleetVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
|
"""Class to manage fetching data from the TeslaFleet API."""
|
||||||
|
|
||||||
|
updated_once: bool
|
||||||
|
pre2021: bool
|
||||||
|
last_active: datetime
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, hass: HomeAssistant, api: VehicleSpecific, product: dict
|
||||||
|
) -> None:
|
||||||
|
"""Initialize TeslaFleet Vehicle Update Coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
LOGGER,
|
||||||
|
name="Tesla Fleet Vehicle",
|
||||||
|
update_interval=timedelta(seconds=5),
|
||||||
|
)
|
||||||
|
self.api = api
|
||||||
|
self.data = flatten(product)
|
||||||
|
self.updated_once = False
|
||||||
|
self.last_active = datetime.now()
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, Any]:
|
||||||
|
"""Update vehicle data using TeslaFleet API."""
|
||||||
|
|
||||||
|
self.update_interval = VEHICLE_INTERVAL
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if the vehicle is awake using a non-rate limited API call
|
||||||
|
state = (await self.api.vehicle())["response"]
|
||||||
|
if state and state["state"] != TeslaFleetState.ONLINE:
|
||||||
|
self.data["state"] = state["state"]
|
||||||
|
return self.data
|
||||||
|
|
||||||
|
# This is a rated limited API call
|
||||||
|
data = (await self.api.vehicle_data(endpoints=ENDPOINTS))["response"]
|
||||||
|
except VehicleOffline:
|
||||||
|
self.data["state"] = TeslaFleetState.ASLEEP
|
||||||
|
return self.data
|
||||||
|
except RateLimited as e:
|
||||||
|
LOGGER.warning(
|
||||||
|
"%s rate limited, will retry in %s seconds",
|
||||||
|
self.name,
|
||||||
|
e.data.get("after"),
|
||||||
|
)
|
||||||
|
if "after" in e.data:
|
||||||
|
self.update_interval = timedelta(seconds=int(e.data["after"]))
|
||||||
|
return self.data
|
||||||
|
except (InvalidToken, OAuthExpired, LoginRequired) as e:
|
||||||
|
raise ConfigEntryAuthFailed from e
|
||||||
|
except TeslaFleetError as e:
|
||||||
|
raise UpdateFailed(e.message) from e
|
||||||
|
|
||||||
|
self.updated_once = True
|
||||||
|
|
||||||
|
if self.api.pre2021 and data["state"] == TeslaFleetState.ONLINE:
|
||||||
|
# Handle pre-2021 vehicles which cannot sleep by themselves
|
||||||
|
if (
|
||||||
|
data["charge_state"].get("charging_state") == "Charging"
|
||||||
|
or data["vehicle_state"].get("is_user_present")
|
||||||
|
or data["vehicle_state"].get("sentry_mode")
|
||||||
|
):
|
||||||
|
# Vehicle is active, reset timer
|
||||||
|
self.last_active = datetime.now()
|
||||||
|
else:
|
||||||
|
elapsed = datetime.now() - self.last_active
|
||||||
|
if elapsed > timedelta(minutes=20):
|
||||||
|
# Vehicle didn't sleep, try again in 15 minutes
|
||||||
|
self.last_active = datetime.now()
|
||||||
|
elif elapsed > timedelta(minutes=15):
|
||||||
|
# Let vehicle go to sleep now
|
||||||
|
self.update_interval = VEHICLE_WAIT
|
||||||
|
|
||||||
|
return flatten(data)
|
||||||
|
|
||||||
|
|
||||||
|
class TeslaFleetEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
|
"""Class to manage fetching energy site live status from the TeslaFleet API."""
|
||||||
|
|
||||||
|
updated_once: bool
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, api: EnergySpecific) -> None:
|
||||||
|
"""Initialize TeslaFleet Energy Site Live coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
LOGGER,
|
||||||
|
name="Tesla Fleet Energy Site Live",
|
||||||
|
update_interval=timedelta(seconds=10),
|
||||||
|
)
|
||||||
|
self.api = api
|
||||||
|
self.data = {}
|
||||||
|
self.updated_once = False
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, Any]:
|
||||||
|
"""Update energy site data using TeslaFleet API."""
|
||||||
|
|
||||||
|
self.update_interval = ENERGY_INTERVAL
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = (await self.api.live_status())["response"]
|
||||||
|
except RateLimited as e:
|
||||||
|
LOGGER.warning(
|
||||||
|
"%s rate limited, will retry in %s seconds",
|
||||||
|
self.name,
|
||||||
|
e.data.get("after"),
|
||||||
|
)
|
||||||
|
if "after" in e.data:
|
||||||
|
self.update_interval = timedelta(seconds=int(e.data["after"]))
|
||||||
|
return self.data
|
||||||
|
except (InvalidToken, OAuthExpired, LoginRequired) as e:
|
||||||
|
raise ConfigEntryAuthFailed from e
|
||||||
|
except TeslaFleetError as e:
|
||||||
|
raise UpdateFailed(e.message) from e
|
||||||
|
|
||||||
|
# Convert Wall Connectors from array to dict
|
||||||
|
data["wall_connectors"] = {
|
||||||
|
wc["din"]: wc for wc in (data.get("wall_connectors") or [])
|
||||||
|
}
|
||||||
|
|
||||||
|
self.updated_once = True
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class TeslaFleetEnergySiteInfoCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
|
"""Class to manage fetching energy site info from the TeslaFleet API."""
|
||||||
|
|
||||||
|
updated_once: bool
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, api: EnergySpecific, product: dict) -> None:
|
||||||
|
"""Initialize TeslaFleet Energy Info coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
LOGGER,
|
||||||
|
name="Tesla Fleet Energy Site Info",
|
||||||
|
update_interval=timedelta(seconds=15),
|
||||||
|
)
|
||||||
|
self.api = api
|
||||||
|
self.data = flatten(product)
|
||||||
|
self.updated_once = False
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, Any]:
|
||||||
|
"""Update energy site data using TeslaFleet API."""
|
||||||
|
|
||||||
|
self.update_interval = ENERGY_INTERVAL
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = (await self.api.site_info())["response"]
|
||||||
|
except RateLimited as e:
|
||||||
|
LOGGER.warning(
|
||||||
|
"%s rate limited, will retry in %s seconds",
|
||||||
|
self.name,
|
||||||
|
e.data.get("after"),
|
||||||
|
)
|
||||||
|
if "after" in e.data:
|
||||||
|
self.update_interval = timedelta(seconds=int(e.data["after"]))
|
||||||
|
return self.data
|
||||||
|
except (InvalidToken, OAuthExpired, LoginRequired) as e:
|
||||||
|
raise ConfigEntryAuthFailed from e
|
||||||
|
except TeslaFleetError as e:
|
||||||
|
raise UpdateFailed(e.message) from e
|
||||||
|
|
||||||
|
self.updated_once = True
|
||||||
|
return flatten(data)
|
176
homeassistant/components/tesla_fleet/entity.py
Normal file
176
homeassistant/components/tesla_fleet/entity.py
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
"""Tesla Fleet parent entity class."""
|
||||||
|
|
||||||
|
from abc import abstractmethod
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from tesla_fleet_api import EnergySpecific, VehicleSpecific
|
||||||
|
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import (
|
||||||
|
TeslaFleetEnergySiteInfoCoordinator,
|
||||||
|
TeslaFleetEnergySiteLiveCoordinator,
|
||||||
|
TeslaFleetVehicleDataCoordinator,
|
||||||
|
)
|
||||||
|
from .models import TeslaFleetEnergyData, TeslaFleetVehicleData
|
||||||
|
|
||||||
|
|
||||||
|
class TeslaFleetEntity(
|
||||||
|
CoordinatorEntity[
|
||||||
|
TeslaFleetVehicleDataCoordinator
|
||||||
|
| TeslaFleetEnergySiteLiveCoordinator
|
||||||
|
| TeslaFleetEnergySiteInfoCoordinator
|
||||||
|
]
|
||||||
|
):
|
||||||
|
"""Parent class for all TeslaFleet entities."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: TeslaFleetVehicleDataCoordinator
|
||||||
|
| TeslaFleetEnergySiteLiveCoordinator
|
||||||
|
| TeslaFleetEnergySiteInfoCoordinator,
|
||||||
|
api: VehicleSpecific | EnergySpecific,
|
||||||
|
key: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize common aspects of a TeslaFleet entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.api = api
|
||||||
|
self.key = key
|
||||||
|
self._attr_translation_key = self.key
|
||||||
|
self._async_update_attrs()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return if sensor is available."""
|
||||||
|
return self.coordinator.last_update_success and self._attr_available
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _value(self) -> Any | None:
|
||||||
|
"""Return a specific value from coordinator data."""
|
||||||
|
return self.coordinator.data.get(self.key)
|
||||||
|
|
||||||
|
def get(self, key: str, default: Any | None = None) -> Any | None:
|
||||||
|
"""Return a specific value from coordinator data."""
|
||||||
|
return self.coordinator.data.get(key, default)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_none(self) -> bool:
|
||||||
|
"""Return if the value is a literal None."""
|
||||||
|
return self.get(self.key, False) is None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has(self) -> bool:
|
||||||
|
"""Return True if a specific value is in coordinator data."""
|
||||||
|
return self.key in self.coordinator.data
|
||||||
|
|
||||||
|
def _handle_coordinator_update(self) -> None:
|
||||||
|
"""Handle updated data from the coordinator."""
|
||||||
|
self._async_update_attrs()
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update the attributes of the entity."""
|
||||||
|
|
||||||
|
|
||||||
|
class TeslaFleetVehicleEntity(TeslaFleetEntity):
|
||||||
|
"""Parent class for TeslaFleet Vehicle entities."""
|
||||||
|
|
||||||
|
_last_update: int = 0
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
data: TeslaFleetVehicleData,
|
||||||
|
key: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize common aspects of a Tesla Fleet entity."""
|
||||||
|
|
||||||
|
self._attr_unique_id = f"{data.vin}-{key}"
|
||||||
|
self.vehicle = data
|
||||||
|
|
||||||
|
self._attr_device_info = data.device
|
||||||
|
super().__init__(data.coordinator, data.api, key)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _value(self) -> Any | None:
|
||||||
|
"""Return a specific value from coordinator data."""
|
||||||
|
return self.coordinator.data.get(self.key)
|
||||||
|
|
||||||
|
|
||||||
|
class TeslaFleetEnergyLiveEntity(TeslaFleetEntity):
|
||||||
|
"""Parent class for TeslaFleet Energy Site Live entities."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
data: TeslaFleetEnergyData,
|
||||||
|
key: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize common aspects of a Tesla Fleet Energy Site Live entity."""
|
||||||
|
self._attr_unique_id = f"{data.id}-{key}"
|
||||||
|
self._attr_device_info = data.device
|
||||||
|
|
||||||
|
super().__init__(data.live_coordinator, data.api, key)
|
||||||
|
|
||||||
|
|
||||||
|
class TeslaFleetEnergyInfoEntity(TeslaFleetEntity):
|
||||||
|
"""Parent class for TeslaFleet Energy Site Info entities."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
data: TeslaFleetEnergyData,
|
||||||
|
key: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize common aspects of a Tesla Fleet Energy Site Info entity."""
|
||||||
|
self._attr_unique_id = f"{data.id}-{key}"
|
||||||
|
self._attr_device_info = data.device
|
||||||
|
|
||||||
|
super().__init__(data.info_coordinator, data.api, key)
|
||||||
|
|
||||||
|
|
||||||
|
class TeslaFleetWallConnectorEntity(
|
||||||
|
TeslaFleetEntity, CoordinatorEntity[TeslaFleetEnergySiteLiveCoordinator]
|
||||||
|
):
|
||||||
|
"""Parent class for Tesla Fleet Wall Connector entities."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
data: TeslaFleetEnergyData,
|
||||||
|
din: str,
|
||||||
|
key: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize common aspects of a Tesla Fleet entity."""
|
||||||
|
self.din = din
|
||||||
|
self._attr_unique_id = f"{data.id}-{din}-{key}"
|
||||||
|
|
||||||
|
# Find the model from the info coordinator
|
||||||
|
model: str | None = None
|
||||||
|
for wc in data.info_coordinator.data.get("components_wall_connectors", []):
|
||||||
|
if wc["din"] == din:
|
||||||
|
model = wc.get("part_name")
|
||||||
|
break
|
||||||
|
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, din)},
|
||||||
|
manufacturer="Tesla",
|
||||||
|
name="Wall Connector",
|
||||||
|
via_device=(DOMAIN, str(data.id)),
|
||||||
|
serial_number=din.split("-")[-1],
|
||||||
|
model=model,
|
||||||
|
)
|
||||||
|
|
||||||
|
super().__init__(data.live_coordinator, data.api, key)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _value(self) -> int:
|
||||||
|
"""Return a specific wall connector value from coordinator data."""
|
||||||
|
return (
|
||||||
|
self.coordinator.data.get("wall_connectors", {})
|
||||||
|
.get(self.din, {})
|
||||||
|
.get(self.key)
|
||||||
|
)
|
66
homeassistant/components/tesla_fleet/icons.json
Normal file
66
homeassistant/components/tesla_fleet/icons.json
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"battery_power": {
|
||||||
|
"default": "mdi:home-battery"
|
||||||
|
},
|
||||||
|
"charge_state_charging_state": {
|
||||||
|
"default": "mdi:ev-station",
|
||||||
|
"state": {
|
||||||
|
"disconnected": "mdi:connection",
|
||||||
|
"no_power": "mdi:power-plug-off-outline",
|
||||||
|
"starting": "mdi:play-circle",
|
||||||
|
"stopped": "mdi:stop-circle"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"drive_state_active_route_destination": {
|
||||||
|
"default": "mdi:routes"
|
||||||
|
},
|
||||||
|
"drive_state_active_route_minutes_to_arrival": {
|
||||||
|
"default": "mdi:routes-clock"
|
||||||
|
},
|
||||||
|
"drive_state_shift_state": {
|
||||||
|
"default": "mdi:car-shift-pattern",
|
||||||
|
"state": {
|
||||||
|
"d": "mdi:alpha-d",
|
||||||
|
"n": "mdi:alpha-n",
|
||||||
|
"p": "mdi:alpha-p",
|
||||||
|
"r": "mdi:alpha-r"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"energy_left": {
|
||||||
|
"default": "mdi:battery"
|
||||||
|
},
|
||||||
|
"generator_power": {
|
||||||
|
"default": "mdi:generator-stationary"
|
||||||
|
},
|
||||||
|
"grid_power": {
|
||||||
|
"default": "mdi:transmission-tower"
|
||||||
|
},
|
||||||
|
"grid_services_power": {
|
||||||
|
"default": "mdi:transmission-tower"
|
||||||
|
},
|
||||||
|
"load_power": {
|
||||||
|
"default": "mdi:power-plug"
|
||||||
|
},
|
||||||
|
"solar_power": {
|
||||||
|
"default": "mdi:solar-power"
|
||||||
|
},
|
||||||
|
"total_pack_energy": {
|
||||||
|
"default": "mdi:battery-high"
|
||||||
|
},
|
||||||
|
"vin": {
|
||||||
|
"default": "mdi:car-electric"
|
||||||
|
},
|
||||||
|
"wall_connector_fault_state": {
|
||||||
|
"default": "mdi:ev-station"
|
||||||
|
},
|
||||||
|
"wall_connector_power": {
|
||||||
|
"default": "mdi:ev-station"
|
||||||
|
},
|
||||||
|
"wall_connector_state": {
|
||||||
|
"default": "mdi:ev-station"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
homeassistant/components/tesla_fleet/manifest.json
Normal file
11
homeassistant/components/tesla_fleet/manifest.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"domain": "tesla_fleet",
|
||||||
|
"name": "Tesla Fleet",
|
||||||
|
"codeowners": ["@Bre77"],
|
||||||
|
"config_flow": true,
|
||||||
|
"dependencies": ["application_credentials", "http"],
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/tesla_fleet",
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
"loggers": ["tesla-fleet-api"],
|
||||||
|
"requirements": ["tesla-fleet-api==0.7.2"]
|
||||||
|
}
|
46
homeassistant/components/tesla_fleet/models.py
Normal file
46
homeassistant/components/tesla_fleet/models.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
"""The Tesla Fleet integration models."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from tesla_fleet_api import EnergySpecific, VehicleSpecific
|
||||||
|
from tesla_fleet_api.const import Scope
|
||||||
|
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
|
||||||
|
from .coordinator import (
|
||||||
|
TeslaFleetEnergySiteInfoCoordinator,
|
||||||
|
TeslaFleetEnergySiteLiveCoordinator,
|
||||||
|
TeslaFleetVehicleDataCoordinator,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TeslaFleetData:
|
||||||
|
"""Data for the TeslaFleet integration."""
|
||||||
|
|
||||||
|
vehicles: list[TeslaFleetVehicleData]
|
||||||
|
energysites: list[TeslaFleetEnergyData]
|
||||||
|
scopes: list[Scope]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TeslaFleetVehicleData:
|
||||||
|
"""Data for a vehicle in the TeslaFleet integration."""
|
||||||
|
|
||||||
|
api: VehicleSpecific
|
||||||
|
coordinator: TeslaFleetVehicleDataCoordinator
|
||||||
|
vin: str
|
||||||
|
device: DeviceInfo
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TeslaFleetEnergyData:
|
||||||
|
"""Data for a vehicle in the TeslaFleet integration."""
|
||||||
|
|
||||||
|
api: EnergySpecific
|
||||||
|
live_coordinator: TeslaFleetEnergySiteLiveCoordinator
|
||||||
|
info_coordinator: TeslaFleetEnergySiteInfoCoordinator
|
||||||
|
id: int
|
||||||
|
device: DeviceInfo
|
599
homeassistant/components/tesla_fleet/sensor.py
Normal file
599
homeassistant/components/tesla_fleet/sensor.py
Normal file
|
@ -0,0 +1,599 @@
|
||||||
|
"""Sensor platform for Tesla Fleet integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import timedelta
|
||||||
|
from itertools import chain
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
RestoreSensor,
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
PERCENTAGE,
|
||||||
|
EntityCategory,
|
||||||
|
UnitOfElectricCurrent,
|
||||||
|
UnitOfElectricPotential,
|
||||||
|
UnitOfEnergy,
|
||||||
|
UnitOfLength,
|
||||||
|
UnitOfPower,
|
||||||
|
UnitOfPressure,
|
||||||
|
UnitOfSpeed,
|
||||||
|
UnitOfTemperature,
|
||||||
|
UnitOfTime,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.typing import StateType
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
from homeassistant.util.variance import ignore_variance
|
||||||
|
|
||||||
|
from . import TeslaFleetConfigEntry
|
||||||
|
from .const import TeslaFleetState
|
||||||
|
from .entity import (
|
||||||
|
TeslaFleetEnergyInfoEntity,
|
||||||
|
TeslaFleetEnergyLiveEntity,
|
||||||
|
TeslaFleetVehicleEntity,
|
||||||
|
TeslaFleetWallConnectorEntity,
|
||||||
|
)
|
||||||
|
from .models import TeslaFleetEnergyData, TeslaFleetVehicleData
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
CHARGE_STATES = {
|
||||||
|
"Starting": "starting",
|
||||||
|
"Charging": "charging",
|
||||||
|
"Stopped": "stopped",
|
||||||
|
"Complete": "complete",
|
||||||
|
"Disconnected": "disconnected",
|
||||||
|
"NoPower": "no_power",
|
||||||
|
}
|
||||||
|
|
||||||
|
SHIFT_STATES = {"P": "p", "D": "d", "R": "r", "N": "n"}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class TeslaFleetSensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Describes Tesla Fleet Sensor entity."""
|
||||||
|
|
||||||
|
value_fn: Callable[[StateType], StateType] = lambda x: x
|
||||||
|
|
||||||
|
|
||||||
|
VEHICLE_DESCRIPTIONS: tuple[TeslaFleetSensorEntityDescription, ...] = (
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="charge_state_charging_state",
|
||||||
|
options=list(CHARGE_STATES.values()),
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
value_fn=lambda value: CHARGE_STATES.get(cast(str, value)),
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="charge_state_battery_level",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
device_class=SensorDeviceClass.BATTERY,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="charge_state_usable_battery_level",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
device_class=SensorDeviceClass.BATTERY,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="charge_state_charge_energy_added",
|
||||||
|
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||||
|
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||||
|
device_class=SensorDeviceClass.ENERGY,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="charge_state_charger_power",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||||
|
device_class=SensorDeviceClass.POWER,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="charge_state_charger_voltage",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||||
|
device_class=SensorDeviceClass.VOLTAGE,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="charge_state_charger_actual_current",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||||
|
device_class=SensorDeviceClass.CURRENT,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="charge_state_charge_rate",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
|
||||||
|
device_class=SensorDeviceClass.SPEED,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="charge_state_conn_charge_cable",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="charge_state_fast_charger_type",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="charge_state_battery_range",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfLength.MILES,
|
||||||
|
device_class=SensorDeviceClass.DISTANCE,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="charge_state_est_battery_range",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfLength.MILES,
|
||||||
|
device_class=SensorDeviceClass.DISTANCE,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="charge_state_ideal_battery_range",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfLength.MILES,
|
||||||
|
device_class=SensorDeviceClass.DISTANCE,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="drive_state_speed",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
|
||||||
|
device_class=SensorDeviceClass.SPEED,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
value_fn=lambda value: value or 0,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="drive_state_power",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||||
|
device_class=SensorDeviceClass.POWER,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
value_fn=lambda value: value or 0,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="drive_state_shift_state",
|
||||||
|
options=list(SHIFT_STATES.values()),
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
value_fn=lambda x: SHIFT_STATES.get(str(x), "p"),
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="vehicle_state_odometer",
|
||||||
|
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||||
|
native_unit_of_measurement=UnitOfLength.MILES,
|
||||||
|
device_class=SensorDeviceClass.DISTANCE,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="vehicle_state_tpms_pressure_fl",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPressure.BAR,
|
||||||
|
suggested_unit_of_measurement=UnitOfPressure.PSI,
|
||||||
|
device_class=SensorDeviceClass.PRESSURE,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="vehicle_state_tpms_pressure_fr",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPressure.BAR,
|
||||||
|
suggested_unit_of_measurement=UnitOfPressure.PSI,
|
||||||
|
device_class=SensorDeviceClass.PRESSURE,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="vehicle_state_tpms_pressure_rl",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPressure.BAR,
|
||||||
|
suggested_unit_of_measurement=UnitOfPressure.PSI,
|
||||||
|
device_class=SensorDeviceClass.PRESSURE,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="vehicle_state_tpms_pressure_rr",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPressure.BAR,
|
||||||
|
suggested_unit_of_measurement=UnitOfPressure.PSI,
|
||||||
|
device_class=SensorDeviceClass.PRESSURE,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="climate_state_inside_temp",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="climate_state_outside_temp",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="climate_state_driver_temp_setting",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="climate_state_passenger_temp_setting",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="drive_state_active_route_traffic_minutes_delay",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||||
|
device_class=SensorDeviceClass.DURATION,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="drive_state_active_route_energy_at_arrival",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
device_class=SensorDeviceClass.BATTERY,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
TeslaFleetSensorEntityDescription(
|
||||||
|
key="drive_state_active_route_miles_to_arrival",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfLength.MILES,
|
||||||
|
device_class=SensorDeviceClass.DISTANCE,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class TeslaFleetTimeEntityDescription(SensorEntityDescription):
|
||||||
|
"""Describes Tesla Fleet Sensor entity."""
|
||||||
|
|
||||||
|
variance: int
|
||||||
|
|
||||||
|
|
||||||
|
VEHICLE_TIME_DESCRIPTIONS: tuple[TeslaFleetTimeEntityDescription, ...] = (
|
||||||
|
TeslaFleetTimeEntityDescription(
|
||||||
|
key="charge_state_minutes_to_full_charge",
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
variance=4,
|
||||||
|
),
|
||||||
|
TeslaFleetTimeEntityDescription(
|
||||||
|
key="drive_state_active_route_minutes_to_arrival",
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
variance=1,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
ENERGY_LIVE_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="solar_power",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPower.WATT,
|
||||||
|
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
device_class=SensorDeviceClass.POWER,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="energy_left",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||||
|
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="total_pack_energy",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||||
|
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
device_class=SensorDeviceClass.ENERGY_STORAGE,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="percentage_charged",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
device_class=SensorDeviceClass.BATTERY,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="battery_power",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPower.WATT,
|
||||||
|
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
device_class=SensorDeviceClass.POWER,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="load_power",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPower.WATT,
|
||||||
|
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
device_class=SensorDeviceClass.POWER,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="grid_power",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPower.WATT,
|
||||||
|
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
device_class=SensorDeviceClass.POWER,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="grid_services_power",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPower.WATT,
|
||||||
|
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
device_class=SensorDeviceClass.POWER,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="generator_power",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPower.WATT,
|
||||||
|
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
device_class=SensorDeviceClass.POWER,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
WALL_CONNECTOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="wall_connector_state",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="wall_connector_fault_state",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="wall_connector_power",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfPower.WATT,
|
||||||
|
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
device_class=SensorDeviceClass.POWER,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="vin",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
ENERGY_INFO_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="vpp_backup_reserve_percent",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
device_class=SensorDeviceClass.BATTERY,
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(key="version"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: TeslaFleetConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the Tesla Fleet sensor platform from a config entry."""
|
||||||
|
async_add_entities(
|
||||||
|
chain(
|
||||||
|
( # Add vehicles
|
||||||
|
TeslaFleetVehicleSensorEntity(vehicle, description)
|
||||||
|
for vehicle in entry.runtime_data.vehicles
|
||||||
|
for description in VEHICLE_DESCRIPTIONS
|
||||||
|
),
|
||||||
|
( # Add vehicles time sensors
|
||||||
|
TeslaFleetVehicleTimeSensorEntity(vehicle, description)
|
||||||
|
for vehicle in entry.runtime_data.vehicles
|
||||||
|
for description in VEHICLE_TIME_DESCRIPTIONS
|
||||||
|
),
|
||||||
|
( # Add energy site live
|
||||||
|
TeslaFleetEnergyLiveSensorEntity(energysite, description)
|
||||||
|
for energysite in entry.runtime_data.energysites
|
||||||
|
for description in ENERGY_LIVE_DESCRIPTIONS
|
||||||
|
if description.key in energysite.live_coordinator.data
|
||||||
|
),
|
||||||
|
( # Add wall connectors
|
||||||
|
TeslaFleetWallConnectorSensorEntity(energysite, wc["din"], description)
|
||||||
|
for energysite in entry.runtime_data.energysites
|
||||||
|
for wc in energysite.info_coordinator.data.get(
|
||||||
|
"components_wall_connectors", []
|
||||||
|
)
|
||||||
|
if "din" in wc
|
||||||
|
for description in WALL_CONNECTOR_DESCRIPTIONS
|
||||||
|
),
|
||||||
|
( # Add energy site info
|
||||||
|
TeslaFleetEnergyInfoSensorEntity(energysite, description)
|
||||||
|
for energysite in entry.runtime_data.energysites
|
||||||
|
for description in ENERGY_INFO_DESCRIPTIONS
|
||||||
|
if description.key in energysite.info_coordinator.data
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TeslaFleetVehicleSensorEntity(TeslaFleetVehicleEntity, RestoreSensor):
|
||||||
|
"""Base class for Tesla Fleet vehicle metric sensors."""
|
||||||
|
|
||||||
|
entity_description: TeslaFleetSensorEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
data: TeslaFleetVehicleData,
|
||||||
|
description: TeslaFleetSensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self.entity_description = description
|
||||||
|
super().__init__(data, description.key)
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Handle entity which will be added."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
if self.coordinator.data.get("state") == TeslaFleetState.OFFLINE:
|
||||||
|
if (sensor_data := await self.async_get_last_sensor_data()) is not None:
|
||||||
|
self._attr_native_value = sensor_data.native_value
|
||||||
|
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update the attributes of the sensor."""
|
||||||
|
if self.has:
|
||||||
|
self._attr_native_value = self.entity_description.value_fn(self._value)
|
||||||
|
else:
|
||||||
|
self._attr_native_value = None
|
||||||
|
|
||||||
|
|
||||||
|
class TeslaFleetVehicleTimeSensorEntity(TeslaFleetVehicleEntity, SensorEntity):
|
||||||
|
"""Base class for Tesla Fleet vehicle time sensors."""
|
||||||
|
|
||||||
|
entity_description: TeslaFleetTimeEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
data: TeslaFleetVehicleData,
|
||||||
|
description: TeslaFleetTimeEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self.entity_description = description
|
||||||
|
self._get_timestamp = ignore_variance(
|
||||||
|
func=lambda value: dt_util.now() + timedelta(minutes=value),
|
||||||
|
ignored_variance=timedelta(minutes=description.variance),
|
||||||
|
)
|
||||||
|
|
||||||
|
super().__init__(data, description.key)
|
||||||
|
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update the attributes of the sensor."""
|
||||||
|
self._attr_available = isinstance(self._value, int | float) and self._value > 0
|
||||||
|
if self._attr_available:
|
||||||
|
self._attr_native_value = self._get_timestamp(self._value)
|
||||||
|
|
||||||
|
|
||||||
|
class TeslaFleetEnergyLiveSensorEntity(TeslaFleetEnergyLiveEntity, RestoreSensor):
|
||||||
|
"""Base class for Tesla Fleet energy site metric sensors."""
|
||||||
|
|
||||||
|
entity_description: SensorEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
data: TeslaFleetEnergyData,
|
||||||
|
description: SensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self.entity_description = description
|
||||||
|
super().__init__(data, description.key)
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Handle entity which will be added."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
if not self.coordinator.updated_once:
|
||||||
|
if (sensor_data := await self.async_get_last_sensor_data()) is not None:
|
||||||
|
self._attr_native_value = sensor_data.native_value
|
||||||
|
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update the attributes of the sensor."""
|
||||||
|
self._attr_available = not self.is_none
|
||||||
|
self._attr_native_value = self._value
|
||||||
|
|
||||||
|
|
||||||
|
class TeslaFleetWallConnectorSensorEntity(TeslaFleetWallConnectorEntity, RestoreSensor):
|
||||||
|
"""Base class for Tesla Fleet energy site metric sensors."""
|
||||||
|
|
||||||
|
entity_description: SensorEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
data: TeslaFleetEnergyData,
|
||||||
|
din: str,
|
||||||
|
description: SensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self.entity_description = description
|
||||||
|
super().__init__(
|
||||||
|
data,
|
||||||
|
din,
|
||||||
|
description.key,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Handle entity which will be added."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
if not self.coordinator.updated_once:
|
||||||
|
if (sensor_data := await self.async_get_last_sensor_data()) is not None:
|
||||||
|
self._attr_native_value = sensor_data.native_value
|
||||||
|
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update the attributes of the sensor."""
|
||||||
|
self._attr_available = not self.is_none
|
||||||
|
self._attr_native_value = self._value
|
||||||
|
|
||||||
|
|
||||||
|
class TeslaFleetEnergyInfoSensorEntity(TeslaFleetEnergyInfoEntity, RestoreSensor):
|
||||||
|
"""Base class for Tesla Fleet energy site metric sensors."""
|
||||||
|
|
||||||
|
entity_description: SensorEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
data: TeslaFleetEnergyData,
|
||||||
|
description: SensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self.entity_description = description
|
||||||
|
super().__init__(data, description.key)
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Handle entity which will be added."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
if not self.coordinator.updated_once:
|
||||||
|
if (sensor_data := await self.async_get_last_sensor_data()) is not None:
|
||||||
|
self._attr_native_value = sensor_data.native_value
|
||||||
|
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update the attributes of the sensor."""
|
||||||
|
self._attr_available = not self.is_none
|
||||||
|
self._attr_native_value = self._value
|
190
homeassistant/components/tesla_fleet/strings.json
Normal file
190
homeassistant/components/tesla_fleet/strings.json
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
|
||||||
|
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||||
|
"already_configured": "Configuration updated for profile.",
|
||||||
|
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
||||||
|
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
||||||
|
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
||||||
|
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
||||||
|
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"pick_implementation": {
|
||||||
|
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
|
||||||
|
},
|
||||||
|
"reauth_confirm": {
|
||||||
|
"title": "[%key:common::config_flow::title::reauth%]",
|
||||||
|
"description": "The Withings integration needs to re-authenticate your account"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"create_entry": {
|
||||||
|
"default": "Successfully authenticated with Tesla."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"battery_power": {
|
||||||
|
"name": "Battery power"
|
||||||
|
},
|
||||||
|
"charge_state_battery_range": {
|
||||||
|
"name": "Battery range"
|
||||||
|
},
|
||||||
|
"charge_state_est_battery_range": {
|
||||||
|
"name": "Estimate battery range"
|
||||||
|
},
|
||||||
|
"charge_state_ideal_battery_range": {
|
||||||
|
"name": "Ideal battery range"
|
||||||
|
},
|
||||||
|
"charge_state_charge_energy_added": {
|
||||||
|
"name": "Charge energy added"
|
||||||
|
},
|
||||||
|
"charge_state_charge_rate": {
|
||||||
|
"name": "Charge rate"
|
||||||
|
},
|
||||||
|
"charge_state_charger_actual_current": {
|
||||||
|
"name": "Charger current"
|
||||||
|
},
|
||||||
|
"charge_state_charger_power": {
|
||||||
|
"name": "Charger power"
|
||||||
|
},
|
||||||
|
"charge_state_charger_voltage": {
|
||||||
|
"name": "Charger voltage"
|
||||||
|
},
|
||||||
|
"charge_state_conn_charge_cable": {
|
||||||
|
"name": "Charge cable"
|
||||||
|
},
|
||||||
|
"charge_state_fast_charger_type": {
|
||||||
|
"name": "Fast charger type"
|
||||||
|
},
|
||||||
|
"charge_state_charging_state": {
|
||||||
|
"name": "Charging",
|
||||||
|
"state": {
|
||||||
|
"starting": "Starting",
|
||||||
|
"charging": "Charging",
|
||||||
|
"disconnected": "Disconnected",
|
||||||
|
"stopped": "Stopped",
|
||||||
|
"complete": "Complete",
|
||||||
|
"no_power": "No power"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"charge_state_minutes_to_full_charge": {
|
||||||
|
"name": "Time to full charge"
|
||||||
|
},
|
||||||
|
"charge_state_battery_level": {
|
||||||
|
"name": "Battery level"
|
||||||
|
},
|
||||||
|
"charge_state_usable_battery_level": {
|
||||||
|
"name": "Usable battery level"
|
||||||
|
},
|
||||||
|
"climate_state_driver_temp_setting": {
|
||||||
|
"name": "Driver temperature setting"
|
||||||
|
},
|
||||||
|
"climate_state_inside_temp": {
|
||||||
|
"name": "Inside temperature"
|
||||||
|
},
|
||||||
|
"climate_state_outside_temp": {
|
||||||
|
"name": "Outside temperature"
|
||||||
|
},
|
||||||
|
"climate_state_passenger_temp_setting": {
|
||||||
|
"name": "Passenger temperature setting"
|
||||||
|
},
|
||||||
|
"drive_state_active_route_destination": {
|
||||||
|
"name": "Destination"
|
||||||
|
},
|
||||||
|
"drive_state_active_route_energy_at_arrival": {
|
||||||
|
"name": "State of charge at arrival"
|
||||||
|
},
|
||||||
|
"drive_state_active_route_miles_to_arrival": {
|
||||||
|
"name": "Distance to arrival"
|
||||||
|
},
|
||||||
|
"drive_state_active_route_minutes_to_arrival": {
|
||||||
|
"name": "Time to arrival"
|
||||||
|
},
|
||||||
|
"drive_state_active_route_traffic_minutes_delay": {
|
||||||
|
"name": "Traffic delay"
|
||||||
|
},
|
||||||
|
"drive_state_power": {
|
||||||
|
"name": "Power"
|
||||||
|
},
|
||||||
|
"drive_state_shift_state": {
|
||||||
|
"name": "Shift state",
|
||||||
|
"state": {
|
||||||
|
"d": "Drive",
|
||||||
|
"n": "Neutral",
|
||||||
|
"p": "Park",
|
||||||
|
"r": "Reverse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"drive_state_speed": {
|
||||||
|
"name": "Speed"
|
||||||
|
},
|
||||||
|
"energy_left": {
|
||||||
|
"name": "Energy left"
|
||||||
|
},
|
||||||
|
"generator_power": {
|
||||||
|
"name": "Generator power"
|
||||||
|
},
|
||||||
|
"grid_power": {
|
||||||
|
"name": "Grid power"
|
||||||
|
},
|
||||||
|
"grid_services_power": {
|
||||||
|
"name": "Grid services power"
|
||||||
|
},
|
||||||
|
"load_power": {
|
||||||
|
"name": "Load power"
|
||||||
|
},
|
||||||
|
"percentage_charged": {
|
||||||
|
"name": "Percentage charged"
|
||||||
|
},
|
||||||
|
"solar_power": {
|
||||||
|
"name": "Solar power"
|
||||||
|
},
|
||||||
|
"total_pack_energy": {
|
||||||
|
"name": "Total pack energy"
|
||||||
|
},
|
||||||
|
"vehicle_state_odometer": {
|
||||||
|
"name": "Odometer"
|
||||||
|
},
|
||||||
|
"vehicle_state_tpms_pressure_fl": {
|
||||||
|
"name": "Tire pressure front left"
|
||||||
|
},
|
||||||
|
"vehicle_state_tpms_pressure_fr": {
|
||||||
|
"name": "Tire pressure front right"
|
||||||
|
},
|
||||||
|
"vehicle_state_tpms_pressure_rl": {
|
||||||
|
"name": "Tire pressure rear left"
|
||||||
|
},
|
||||||
|
"vehicle_state_tpms_pressure_rr": {
|
||||||
|
"name": "Tire pressure rear right"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"name": "version"
|
||||||
|
},
|
||||||
|
"vin": {
|
||||||
|
"name": "Vehicle"
|
||||||
|
},
|
||||||
|
"vpp_backup_reserve_percent": {
|
||||||
|
"name": "VPP backup reserve"
|
||||||
|
},
|
||||||
|
"wall_connector_fault_state": {
|
||||||
|
"name": "Fault state code"
|
||||||
|
},
|
||||||
|
"wall_connector_power": {
|
||||||
|
"name": "Power"
|
||||||
|
},
|
||||||
|
"wall_connector_state": {
|
||||||
|
"name": "State code"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"update_failed": {
|
||||||
|
"message": "{endpoint} data request failed. {message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,6 +24,7 @@ APPLICATION_CREDENTIALS = [
|
||||||
"netatmo",
|
"netatmo",
|
||||||
"senz",
|
"senz",
|
||||||
"spotify",
|
"spotify",
|
||||||
|
"tesla_fleet",
|
||||||
"twitch",
|
"twitch",
|
||||||
"withings",
|
"withings",
|
||||||
"xbox",
|
"xbox",
|
||||||
|
|
|
@ -565,6 +565,7 @@ FLOWS = {
|
||||||
"technove",
|
"technove",
|
||||||
"tedee",
|
"tedee",
|
||||||
"tellduslive",
|
"tellduslive",
|
||||||
|
"tesla_fleet",
|
||||||
"tesla_wall_connector",
|
"tesla_wall_connector",
|
||||||
"teslemetry",
|
"teslemetry",
|
||||||
"tessie",
|
"tessie",
|
||||||
|
|
|
@ -6121,6 +6121,12 @@
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"name": "Tesla Wall Connector"
|
"name": "Tesla Wall Connector"
|
||||||
|
},
|
||||||
|
"tesla_fleet": {
|
||||||
|
"integration_type": "hub",
|
||||||
|
"config_flow": true,
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
"name": "Tesla Fleet"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -2711,6 +2711,7 @@ temperusb==1.6.1
|
||||||
# homeassistant.components.tensorflow
|
# homeassistant.components.tensorflow
|
||||||
# tensorflow==2.5.0
|
# tensorflow==2.5.0
|
||||||
|
|
||||||
|
# homeassistant.components.tesla_fleet
|
||||||
# homeassistant.components.teslemetry
|
# homeassistant.components.teslemetry
|
||||||
# homeassistant.components.tessie
|
# homeassistant.components.tessie
|
||||||
tesla-fleet-api==0.7.2
|
tesla-fleet-api==0.7.2
|
||||||
|
|
|
@ -2121,6 +2121,7 @@ temescal==0.5
|
||||||
# homeassistant.components.temper
|
# homeassistant.components.temper
|
||||||
temperusb==1.6.1
|
temperusb==1.6.1
|
||||||
|
|
||||||
|
# homeassistant.components.tesla_fleet
|
||||||
# homeassistant.components.teslemetry
|
# homeassistant.components.teslemetry
|
||||||
# homeassistant.components.tessie
|
# homeassistant.components.tessie
|
||||||
tesla-fleet-api==0.7.2
|
tesla-fleet-api==0.7.2
|
||||||
|
|
60
tests/components/tesla_fleet/__init__.py
Normal file
60
tests/components/tesla_fleet/__init__.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
"""Tests for the Tesla Fleet integration."""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_platform(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
platforms: list[Platform] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the Tesla Fleet platform."""
|
||||||
|
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
if platforms is None:
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
else:
|
||||||
|
with patch("homeassistant.components.tesla_fleet.PLATFORMS", platforms):
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
|
def assert_entities(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry_id: str,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test that all entities match their snapshot."""
|
||||||
|
|
||||||
|
entity_entries = er.async_entries_for_config_entry(entity_registry, entry_id)
|
||||||
|
|
||||||
|
assert entity_entries
|
||||||
|
for entity_entry in entity_entries:
|
||||||
|
assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry")
|
||||||
|
assert (state := hass.states.get(entity_entry.entity_id))
|
||||||
|
assert state == snapshot(name=f"{entity_entry.entity_id}-state")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_entities_alt(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry_id: str,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test that all entities match their alt snapshot."""
|
||||||
|
entity_entries = er.async_entries_for_config_entry(entity_registry, entry_id)
|
||||||
|
|
||||||
|
assert entity_entries
|
||||||
|
for entity_entry in entity_entries:
|
||||||
|
assert (state := hass.states.get(entity_entry.entity_id))
|
||||||
|
assert state == snapshot(name=f"{entity_entry.entity_id}-statealt")
|
142
tests/components/tesla_fleet/conftest.py
Normal file
142
tests/components/tesla_fleet/conftest.py
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
"""Fixtures for Tessie."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
|
import time
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.application_credentials import (
|
||||||
|
ClientCredential,
|
||||||
|
async_import_client_credential,
|
||||||
|
)
|
||||||
|
from homeassistant.components.tesla_fleet.application_credentials import CLIENT_ID
|
||||||
|
from homeassistant.components.tesla_fleet.const import DOMAIN, SCOPES
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from .const import LIVE_STATUS, PRODUCTS, SITE_INFO, VEHICLE_DATA, VEHICLE_ONLINE
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
UID = "abc-123"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="expires_at")
|
||||||
|
def mock_expires_at() -> int:
|
||||||
|
"""Fixture to set the oauth token expiration time."""
|
||||||
|
return time.time() + 3600
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="scopes")
|
||||||
|
def mock_scopes() -> list[str]:
|
||||||
|
"""Fixture to set the scopes present in the OAuth token."""
|
||||||
|
return SCOPES
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def normal_config_entry(expires_at: int, scopes: list[str]) -> MockConfigEntry:
|
||||||
|
"""Create Tesla Fleet entry in Home Assistant."""
|
||||||
|
|
||||||
|
access_token = jwt.encode(
|
||||||
|
{
|
||||||
|
"sub": UID,
|
||||||
|
"aud": [],
|
||||||
|
"scp": scopes,
|
||||||
|
"ou_code": "NA",
|
||||||
|
},
|
||||||
|
key="",
|
||||||
|
algorithm="none",
|
||||||
|
)
|
||||||
|
|
||||||
|
return MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title=UID,
|
||||||
|
unique_id=UID,
|
||||||
|
data={
|
||||||
|
"auth_implementation": DOMAIN,
|
||||||
|
"token": {
|
||||||
|
"status": 0,
|
||||||
|
"userid": UID,
|
||||||
|
"access_token": access_token,
|
||||||
|
"refresh_token": "mock-refresh-token",
|
||||||
|
"expires_at": expires_at,
|
||||||
|
"scope": ",".join(scopes),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
async def setup_credentials(hass: HomeAssistant) -> None:
|
||||||
|
"""Fixture to setup credentials."""
|
||||||
|
assert await async_setup_component(hass, "application_credentials", {})
|
||||||
|
await async_import_client_credential(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
ClientCredential(CLIENT_ID, ""),
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def mock_products():
|
||||||
|
"""Mock Tesla Fleet Api products method."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.tesla_fleet.TeslaFleetApi.products",
|
||||||
|
return_value=PRODUCTS,
|
||||||
|
) as mock_products:
|
||||||
|
yield mock_products
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def mock_vehicle_state():
|
||||||
|
"""Mock Tesla Fleet API Vehicle Specific vehicle method."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.tesla_fleet.VehicleSpecific.vehicle",
|
||||||
|
return_value=VEHICLE_ONLINE,
|
||||||
|
) as mock_vehicle:
|
||||||
|
yield mock_vehicle
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def mock_vehicle_data():
|
||||||
|
"""Mock Tesla Fleet API Vehicle Specific vehicle_data method."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.tesla_fleet.VehicleSpecific.vehicle_data",
|
||||||
|
return_value=VEHICLE_DATA,
|
||||||
|
) as mock_vehicle_data:
|
||||||
|
yield mock_vehicle_data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def mock_wake_up():
|
||||||
|
"""Mock Tesla Fleet API Vehicle Specific wake_up method."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.tesla_fleet.VehicleSpecific.wake_up",
|
||||||
|
return_value=VEHICLE_ONLINE,
|
||||||
|
) as mock_wake_up:
|
||||||
|
yield mock_wake_up
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def mock_live_status():
|
||||||
|
"""Mock Teslemetry Energy Specific live_status method."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.tesla_fleet.EnergySpecific.live_status",
|
||||||
|
side_effect=lambda: deepcopy(LIVE_STATUS),
|
||||||
|
) as mock_live_status:
|
||||||
|
yield mock_live_status
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def mock_site_info():
|
||||||
|
"""Mock Teslemetry Energy Specific site_info method."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.tesla_fleet.EnergySpecific.site_info",
|
||||||
|
side_effect=lambda: deepcopy(SITE_INFO),
|
||||||
|
) as mock_live_status:
|
||||||
|
yield mock_live_status
|
28
tests/components/tesla_fleet/const.py
Normal file
28
tests/components/tesla_fleet/const.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
"""Constants for the Tesla Fleet tests."""
|
||||||
|
|
||||||
|
from homeassistant.components.tesla_fleet.const import DOMAIN, TeslaFleetState
|
||||||
|
|
||||||
|
from tests.common import load_json_object_fixture
|
||||||
|
|
||||||
|
VEHICLE_ONLINE = {"response": {"state": TeslaFleetState.ONLINE}, "error": None}
|
||||||
|
VEHICLE_ASLEEP = {"response": {"state": TeslaFleetState.ASLEEP}, "error": None}
|
||||||
|
|
||||||
|
PRODUCTS = load_json_object_fixture("products.json", DOMAIN)
|
||||||
|
VEHICLE_DATA = load_json_object_fixture("vehicle_data.json", DOMAIN)
|
||||||
|
VEHICLE_DATA_ALT = load_json_object_fixture("vehicle_data_alt.json", DOMAIN)
|
||||||
|
LIVE_STATUS = load_json_object_fixture("live_status.json", DOMAIN)
|
||||||
|
SITE_INFO = load_json_object_fixture("site_info.json", DOMAIN)
|
||||||
|
|
||||||
|
COMMAND_OK = {"response": {"result": True, "reason": ""}}
|
||||||
|
COMMAND_REASON = {"response": {"result": False, "reason": "already closed"}}
|
||||||
|
COMMAND_IGNORED_REASON = {"response": {"result": False, "reason": "already_set"}}
|
||||||
|
COMMAND_NOREASON = {"response": {"result": False}} # Unexpected
|
||||||
|
COMMAND_ERROR = {
|
||||||
|
"response": None,
|
||||||
|
"error": "vehicle unavailable: vehicle is offline or asleep",
|
||||||
|
"error_description": "",
|
||||||
|
}
|
||||||
|
COMMAND_NOERROR = {"answer": 42}
|
||||||
|
COMMAND_ERRORS = (COMMAND_REASON, COMMAND_NOREASON, COMMAND_ERROR, COMMAND_NOERROR)
|
||||||
|
|
||||||
|
RESPONSE_OK = {"response": {}, "error": None}
|
33
tests/components/tesla_fleet/fixtures/live_status.json
Normal file
33
tests/components/tesla_fleet/fixtures/live_status.json
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"response": {
|
||||||
|
"solar_power": 1185,
|
||||||
|
"energy_left": 38896.47368421053,
|
||||||
|
"total_pack_energy": 40727,
|
||||||
|
"percentage_charged": 95.50537403739663,
|
||||||
|
"backup_capable": true,
|
||||||
|
"battery_power": 5060,
|
||||||
|
"load_power": 6245,
|
||||||
|
"grid_status": "Active",
|
||||||
|
"grid_services_active": false,
|
||||||
|
"grid_power": 0,
|
||||||
|
"grid_services_power": 0,
|
||||||
|
"generator_power": 0,
|
||||||
|
"island_status": "on_grid",
|
||||||
|
"storm_mode_active": false,
|
||||||
|
"timestamp": "2024-01-01T00:00:00+00:00",
|
||||||
|
"wall_connectors": [
|
||||||
|
{
|
||||||
|
"din": "abd-123",
|
||||||
|
"wall_connector_state": 2,
|
||||||
|
"wall_connector_fault_state": 2,
|
||||||
|
"wall_connector_power": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"din": "bcd-234",
|
||||||
|
"wall_connector_state": 2,
|
||||||
|
"wall_connector_fault_state": 2,
|
||||||
|
"wall_connector_power": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
121
tests/components/tesla_fleet/fixtures/products.json
Normal file
121
tests/components/tesla_fleet/fixtures/products.json
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
{
|
||||||
|
"response": [
|
||||||
|
{
|
||||||
|
"id": 1234,
|
||||||
|
"user_id": 1234,
|
||||||
|
"vehicle_id": 1234,
|
||||||
|
"vin": "LRWXF7EK4KC700000",
|
||||||
|
"color": null,
|
||||||
|
"access_type": "OWNER",
|
||||||
|
"display_name": "Test",
|
||||||
|
"option_codes": null,
|
||||||
|
"cached_data": null,
|
||||||
|
"granular_access": { "hide_private": false },
|
||||||
|
"tokens": ["abc", "def"],
|
||||||
|
"state": "asleep",
|
||||||
|
"in_service": false,
|
||||||
|
"id_s": "1234",
|
||||||
|
"calendar_enabled": true,
|
||||||
|
"api_version": 71,
|
||||||
|
"backseat_token": null,
|
||||||
|
"backseat_token_updated_at": null,
|
||||||
|
"ble_autopair_enrolled": false,
|
||||||
|
"vehicle_config": {
|
||||||
|
"aux_park_lamps": "Eu",
|
||||||
|
"badge_version": 1,
|
||||||
|
"can_accept_navigation_requests": true,
|
||||||
|
"can_actuate_trunks": true,
|
||||||
|
"car_special_type": "base",
|
||||||
|
"car_type": "model3",
|
||||||
|
"charge_port_type": "CCS",
|
||||||
|
"cop_user_set_temp_supported": false,
|
||||||
|
"dashcam_clip_save_supported": true,
|
||||||
|
"default_charge_to_max": false,
|
||||||
|
"driver_assist": "TeslaAP3",
|
||||||
|
"ece_restrictions": false,
|
||||||
|
"efficiency_package": "M32021",
|
||||||
|
"eu_vehicle": true,
|
||||||
|
"exterior_color": "DeepBlue",
|
||||||
|
"exterior_trim": "Black",
|
||||||
|
"exterior_trim_override": "",
|
||||||
|
"has_air_suspension": false,
|
||||||
|
"has_ludicrous_mode": false,
|
||||||
|
"has_seat_cooling": false,
|
||||||
|
"headlamp_type": "Global",
|
||||||
|
"interior_trim_type": "White2",
|
||||||
|
"key_version": 2,
|
||||||
|
"motorized_charge_port": true,
|
||||||
|
"paint_color_override": "0,9,25,0.7,0.04",
|
||||||
|
"performance_package": "Base",
|
||||||
|
"plg": true,
|
||||||
|
"pws": true,
|
||||||
|
"rear_drive_unit": "PM216MOSFET",
|
||||||
|
"rear_seat_heaters": 1,
|
||||||
|
"rear_seat_type": 0,
|
||||||
|
"rhd": true,
|
||||||
|
"roof_color": "RoofColorGlass",
|
||||||
|
"seat_type": null,
|
||||||
|
"spoiler_type": "None",
|
||||||
|
"sun_roof_installed": null,
|
||||||
|
"supports_qr_pairing": false,
|
||||||
|
"third_row_seats": "None",
|
||||||
|
"timestamp": 1705701487912,
|
||||||
|
"trim_badging": "74d",
|
||||||
|
"use_range_badging": true,
|
||||||
|
"utc_offset": 36000,
|
||||||
|
"webcam_selfie_supported": true,
|
||||||
|
"webcam_supported": true,
|
||||||
|
"wheel_type": "Pinwheel18CapKit"
|
||||||
|
},
|
||||||
|
"command_signing": "allowed",
|
||||||
|
"release_notes_supported": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"energy_site_id": 123456,
|
||||||
|
"resource_type": "battery",
|
||||||
|
"site_name": "Energy Site",
|
||||||
|
"id": "ABC123",
|
||||||
|
"gateway_id": "ABC123",
|
||||||
|
"asset_site_id": "c0ffee",
|
||||||
|
"warp_site_number": "GA123456",
|
||||||
|
"energy_left": 23286.105263157893,
|
||||||
|
"total_pack_energy": 40804,
|
||||||
|
"percentage_charged": 57.068192488868476,
|
||||||
|
"battery_type": "ac_powerwall",
|
||||||
|
"backup_capable": true,
|
||||||
|
"battery_power": 14990,
|
||||||
|
"go_off_grid_test_banner_enabled": null,
|
||||||
|
"storm_mode_enabled": true,
|
||||||
|
"powerwall_onboarding_settings_set": true,
|
||||||
|
"powerwall_tesla_electric_interested_in": null,
|
||||||
|
"vpp_tour_enabled": null,
|
||||||
|
"sync_grid_alert_enabled": true,
|
||||||
|
"breaker_alert_enabled": true,
|
||||||
|
"components": {
|
||||||
|
"battery": true,
|
||||||
|
"battery_type": "ac_powerwall",
|
||||||
|
"solar": true,
|
||||||
|
"solar_type": "pv_panel",
|
||||||
|
"grid": true,
|
||||||
|
"load_meter": true,
|
||||||
|
"market_type": "residential",
|
||||||
|
"wall_connectors": [
|
||||||
|
{
|
||||||
|
"device_id": "abc-123",
|
||||||
|
"din": "123-abc",
|
||||||
|
"is_active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"device_id": "bcd-234",
|
||||||
|
"din": "234-bcd",
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"rate_plan_manager_no_pricing_constraint": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"count": 2
|
||||||
|
}
|
127
tests/components/tesla_fleet/fixtures/site_info.json
Normal file
127
tests/components/tesla_fleet/fixtures/site_info.json
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
{
|
||||||
|
"response": {
|
||||||
|
"id": "1233-abcd",
|
||||||
|
"site_name": "Site",
|
||||||
|
"backup_reserve_percent": 0,
|
||||||
|
"default_real_mode": "self_consumption",
|
||||||
|
"installation_date": "2022-01-01T00:00:00+00:00",
|
||||||
|
"user_settings": {
|
||||||
|
"go_off_grid_test_banner_enabled": false,
|
||||||
|
"storm_mode_enabled": true,
|
||||||
|
"powerwall_onboarding_settings_set": true,
|
||||||
|
"powerwall_tesla_electric_interested_in": false,
|
||||||
|
"vpp_tour_enabled": true,
|
||||||
|
"sync_grid_alert_enabled": true,
|
||||||
|
"breaker_alert_enabled": false
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"solar": true,
|
||||||
|
"solar_type": "pv_panel",
|
||||||
|
"battery": true,
|
||||||
|
"grid": true,
|
||||||
|
"backup": true,
|
||||||
|
"gateway": "teg",
|
||||||
|
"load_meter": true,
|
||||||
|
"tou_capable": true,
|
||||||
|
"storm_mode_capable": true,
|
||||||
|
"flex_energy_request_capable": false,
|
||||||
|
"car_charging_data_supported": false,
|
||||||
|
"off_grid_vehicle_charging_reserve_supported": true,
|
||||||
|
"vehicle_charging_performance_view_enabled": false,
|
||||||
|
"vehicle_charging_solar_offset_view_enabled": false,
|
||||||
|
"battery_solar_offset_view_enabled": true,
|
||||||
|
"solar_value_enabled": true,
|
||||||
|
"energy_value_header": "Energy Value",
|
||||||
|
"energy_value_subheader": "Estimated Value",
|
||||||
|
"energy_service_self_scheduling_enabled": true,
|
||||||
|
"show_grid_import_battery_source_cards": true,
|
||||||
|
"set_islanding_mode_enabled": true,
|
||||||
|
"wifi_commissioning_enabled": true,
|
||||||
|
"backup_time_remaining_enabled": true,
|
||||||
|
"battery_type": "ac_powerwall",
|
||||||
|
"configurable": true,
|
||||||
|
"grid_services_enabled": false,
|
||||||
|
"gateways": [
|
||||||
|
{
|
||||||
|
"device_id": "gateway-id",
|
||||||
|
"din": "gateway-din",
|
||||||
|
"serial_number": "CN00000000J50D",
|
||||||
|
"part_number": "1152100-14-J",
|
||||||
|
"part_type": 10,
|
||||||
|
"part_name": "Tesla Backup Gateway 2",
|
||||||
|
"is_active": true,
|
||||||
|
"site_id": "1234-abcd",
|
||||||
|
"firmware_version": "24.4.0 0fe780c9",
|
||||||
|
"updated_datetime": "2024-05-14T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"batteries": [
|
||||||
|
{
|
||||||
|
"device_id": "battery-1-id",
|
||||||
|
"din": "battery-1-din",
|
||||||
|
"serial_number": "TG000000001DA5",
|
||||||
|
"part_number": "3012170-10-B",
|
||||||
|
"part_type": 2,
|
||||||
|
"part_name": "Powerwall 2",
|
||||||
|
"nameplate_max_charge_power": 5000,
|
||||||
|
"nameplate_max_discharge_power": 5000,
|
||||||
|
"nameplate_energy": 13500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"device_id": "battery-2-id",
|
||||||
|
"din": "battery-2-din",
|
||||||
|
"serial_number": "TG000000002DA5",
|
||||||
|
"part_number": "3012170-05-C",
|
||||||
|
"part_type": 2,
|
||||||
|
"part_name": "Powerwall 2",
|
||||||
|
"nameplate_max_charge_power": 5000,
|
||||||
|
"nameplate_max_discharge_power": 5000,
|
||||||
|
"nameplate_energy": 13500
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"wall_connectors": [
|
||||||
|
{
|
||||||
|
"device_id": "123abc",
|
||||||
|
"din": "abd-123",
|
||||||
|
"part_name": "Gen 3 Wall Connector",
|
||||||
|
"is_active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"device_id": "234bcd",
|
||||||
|
"din": "bcd-234",
|
||||||
|
"part_name": "Gen 3 Wall Connector",
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"disallow_charge_from_grid_with_solar_installed": true,
|
||||||
|
"customer_preferred_export_rule": "pv_only",
|
||||||
|
"net_meter_mode": "battery_ok",
|
||||||
|
"system_alerts_enabled": true
|
||||||
|
},
|
||||||
|
"version": "23.44.0 eb113390",
|
||||||
|
"battery_count": 2,
|
||||||
|
"tou_settings": {
|
||||||
|
"optimization_strategy": "economics",
|
||||||
|
"schedule": [
|
||||||
|
{
|
||||||
|
"target": "off_peak",
|
||||||
|
"week_days": [1, 0],
|
||||||
|
"start_seconds": 0,
|
||||||
|
"end_seconds": 3600
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "peak",
|
||||||
|
"week_days": [1, 0],
|
||||||
|
"start_seconds": 3600,
|
||||||
|
"end_seconds": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nameplate_power": 15000,
|
||||||
|
"nameplate_energy": 40500,
|
||||||
|
"installation_time_zone": "",
|
||||||
|
"max_site_meter_power_ac": 1000000000,
|
||||||
|
"min_site_meter_power_ac": -1000000000,
|
||||||
|
"vpp_backup_reserve_percent": 0
|
||||||
|
}
|
||||||
|
}
|
282
tests/components/tesla_fleet/fixtures/vehicle_data.json
Normal file
282
tests/components/tesla_fleet/fixtures/vehicle_data.json
Normal file
|
@ -0,0 +1,282 @@
|
||||||
|
{
|
||||||
|
"response": {
|
||||||
|
"id": 1234,
|
||||||
|
"user_id": 1234,
|
||||||
|
"vehicle_id": 1234,
|
||||||
|
"vin": "LRWXF7EK4KC700000",
|
||||||
|
"color": null,
|
||||||
|
"access_type": "OWNER",
|
||||||
|
"granular_access": {
|
||||||
|
"hide_private": false
|
||||||
|
},
|
||||||
|
"tokens": ["abc", "def"],
|
||||||
|
"state": "online",
|
||||||
|
"in_service": false,
|
||||||
|
"id_s": "1234",
|
||||||
|
"calendar_enabled": true,
|
||||||
|
"api_version": 71,
|
||||||
|
"backseat_token": null,
|
||||||
|
"backseat_token_updated_at": null,
|
||||||
|
"ble_autopair_enrolled": false,
|
||||||
|
"charge_state": {
|
||||||
|
"battery_heater_on": false,
|
||||||
|
"battery_level": 77,
|
||||||
|
"battery_range": 266.87,
|
||||||
|
"charge_amps": 16,
|
||||||
|
"charge_current_request": 16,
|
||||||
|
"charge_current_request_max": 16,
|
||||||
|
"charge_enable_request": true,
|
||||||
|
"charge_energy_added": 0,
|
||||||
|
"charge_limit_soc": 80,
|
||||||
|
"charge_limit_soc_max": 100,
|
||||||
|
"charge_limit_soc_min": 50,
|
||||||
|
"charge_limit_soc_std": 80,
|
||||||
|
"charge_miles_added_ideal": 0,
|
||||||
|
"charge_miles_added_rated": 0,
|
||||||
|
"charge_port_cold_weather_mode": false,
|
||||||
|
"charge_port_color": "<invalid>",
|
||||||
|
"charge_port_door_open": true,
|
||||||
|
"charge_port_latch": "Engaged",
|
||||||
|
"charge_rate": 0,
|
||||||
|
"charger_actual_current": 0,
|
||||||
|
"charger_phases": null,
|
||||||
|
"charger_pilot_current": 16,
|
||||||
|
"charger_power": 0,
|
||||||
|
"charger_voltage": 2,
|
||||||
|
"charging_state": "Stopped",
|
||||||
|
"conn_charge_cable": "IEC",
|
||||||
|
"est_battery_range": 275.04,
|
||||||
|
"fast_charger_brand": "<invalid>",
|
||||||
|
"fast_charger_present": false,
|
||||||
|
"fast_charger_type": "ACSingleWireCAN",
|
||||||
|
"ideal_battery_range": 266.87,
|
||||||
|
"max_range_charge_counter": 0,
|
||||||
|
"minutes_to_full_charge": 0,
|
||||||
|
"not_enough_power_to_heat": null,
|
||||||
|
"off_peak_charging_enabled": false,
|
||||||
|
"off_peak_charging_times": "all_week",
|
||||||
|
"off_peak_hours_end_time": 900,
|
||||||
|
"preconditioning_enabled": false,
|
||||||
|
"preconditioning_times": "all_week",
|
||||||
|
"scheduled_charging_mode": "Off",
|
||||||
|
"scheduled_charging_pending": false,
|
||||||
|
"scheduled_charging_start_time": null,
|
||||||
|
"scheduled_charging_start_time_app": 600,
|
||||||
|
"scheduled_departure_time": 1704837600,
|
||||||
|
"scheduled_departure_time_minutes": 480,
|
||||||
|
"supercharger_session_trip_planner": false,
|
||||||
|
"time_to_full_charge": 0,
|
||||||
|
"timestamp": 1705707520649,
|
||||||
|
"trip_charging": false,
|
||||||
|
"usable_battery_level": 77,
|
||||||
|
"user_charge_enable_request": null
|
||||||
|
},
|
||||||
|
"climate_state": {
|
||||||
|
"allow_cabin_overheat_protection": true,
|
||||||
|
"auto_seat_climate_left": true,
|
||||||
|
"auto_seat_climate_right": true,
|
||||||
|
"auto_steering_wheel_heat": false,
|
||||||
|
"battery_heater": false,
|
||||||
|
"battery_heater_no_power": null,
|
||||||
|
"cabin_overheat_protection": "On",
|
||||||
|
"cabin_overheat_protection_actively_cooling": false,
|
||||||
|
"climate_keeper_mode": "keep",
|
||||||
|
"cop_activation_temperature": "High",
|
||||||
|
"defrost_mode": 0,
|
||||||
|
"driver_temp_setting": 22,
|
||||||
|
"fan_status": 0,
|
||||||
|
"hvac_auto_request": "On",
|
||||||
|
"inside_temp": 29.8,
|
||||||
|
"is_auto_conditioning_on": false,
|
||||||
|
"is_climate_on": true,
|
||||||
|
"is_front_defroster_on": false,
|
||||||
|
"is_preconditioning": false,
|
||||||
|
"is_rear_defroster_on": false,
|
||||||
|
"left_temp_direction": 251,
|
||||||
|
"max_avail_temp": 28,
|
||||||
|
"min_avail_temp": 15,
|
||||||
|
"outside_temp": 30,
|
||||||
|
"passenger_temp_setting": 22,
|
||||||
|
"remote_heater_control_enabled": false,
|
||||||
|
"right_temp_direction": 251,
|
||||||
|
"seat_heater_left": 0,
|
||||||
|
"seat_heater_rear_center": 0,
|
||||||
|
"seat_heater_rear_left": 0,
|
||||||
|
"seat_heater_rear_right": 0,
|
||||||
|
"seat_heater_right": 0,
|
||||||
|
"side_mirror_heaters": false,
|
||||||
|
"steering_wheel_heat_level": 0,
|
||||||
|
"steering_wheel_heater": false,
|
||||||
|
"supports_fan_only_cabin_overheat_protection": true,
|
||||||
|
"timestamp": 1705707520649,
|
||||||
|
"wiper_blade_heater": false
|
||||||
|
},
|
||||||
|
"drive_state": {
|
||||||
|
"active_route_latitude": 30.2226265,
|
||||||
|
"active_route_longitude": -97.6236871,
|
||||||
|
"active_route_miles_to_arrival": 0.039491,
|
||||||
|
"active_route_minutes_to_arrival": 0.103577,
|
||||||
|
"active_route_traffic_minutes_delay": 0,
|
||||||
|
"gps_as_of": 1701129612,
|
||||||
|
"heading": 185,
|
||||||
|
"latitude": -30.222626,
|
||||||
|
"longitude": -97.6236871,
|
||||||
|
"native_latitude": -30.222626,
|
||||||
|
"native_location_supported": 1,
|
||||||
|
"native_longitude": -97.6236871,
|
||||||
|
"native_type": "wgs",
|
||||||
|
"power": -7,
|
||||||
|
"shift_state": null,
|
||||||
|
"speed": null,
|
||||||
|
"timestamp": 1705707520649
|
||||||
|
},
|
||||||
|
"gui_settings": {
|
||||||
|
"gui_24_hour_time": false,
|
||||||
|
"gui_charge_rate_units": "kW",
|
||||||
|
"gui_distance_units": "km/hr",
|
||||||
|
"gui_range_display": "Rated",
|
||||||
|
"gui_temperature_units": "C",
|
||||||
|
"gui_tirepressure_units": "Psi",
|
||||||
|
"show_range_units": false,
|
||||||
|
"timestamp": 1705707520649
|
||||||
|
},
|
||||||
|
"vehicle_config": {
|
||||||
|
"aux_park_lamps": "Eu",
|
||||||
|
"badge_version": 1,
|
||||||
|
"can_accept_navigation_requests": true,
|
||||||
|
"can_actuate_trunks": true,
|
||||||
|
"car_special_type": "base",
|
||||||
|
"car_type": "model3",
|
||||||
|
"charge_port_type": "CCS",
|
||||||
|
"cop_user_set_temp_supported": true,
|
||||||
|
"dashcam_clip_save_supported": true,
|
||||||
|
"default_charge_to_max": false,
|
||||||
|
"driver_assist": "TeslaAP3",
|
||||||
|
"ece_restrictions": false,
|
||||||
|
"efficiency_package": "M32021",
|
||||||
|
"eu_vehicle": true,
|
||||||
|
"exterior_color": "DeepBlue",
|
||||||
|
"exterior_trim": "Black",
|
||||||
|
"exterior_trim_override": "",
|
||||||
|
"has_air_suspension": false,
|
||||||
|
"has_ludicrous_mode": false,
|
||||||
|
"has_seat_cooling": false,
|
||||||
|
"headlamp_type": "Global",
|
||||||
|
"interior_trim_type": "White2",
|
||||||
|
"key_version": 2,
|
||||||
|
"motorized_charge_port": true,
|
||||||
|
"paint_color_override": "0,9,25,0.7,0.04",
|
||||||
|
"performance_package": "Base",
|
||||||
|
"plg": true,
|
||||||
|
"pws": true,
|
||||||
|
"rear_drive_unit": "PM216MOSFET",
|
||||||
|
"rear_seat_heaters": 1,
|
||||||
|
"rear_seat_type": 0,
|
||||||
|
"rhd": true,
|
||||||
|
"roof_color": "RoofColorGlass",
|
||||||
|
"seat_type": null,
|
||||||
|
"spoiler_type": "None",
|
||||||
|
"sun_roof_installed": true,
|
||||||
|
"supports_qr_pairing": false,
|
||||||
|
"third_row_seats": "None",
|
||||||
|
"timestamp": 1705707520649,
|
||||||
|
"trim_badging": "74d",
|
||||||
|
"use_range_badging": true,
|
||||||
|
"utc_offset": 36000,
|
||||||
|
"webcam_selfie_supported": true,
|
||||||
|
"webcam_supported": true,
|
||||||
|
"wheel_type": "Pinwheel18CapKit"
|
||||||
|
},
|
||||||
|
"vehicle_state": {
|
||||||
|
"api_version": 71,
|
||||||
|
"autopark_state_v2": "unavailable",
|
||||||
|
"calendar_supported": true,
|
||||||
|
"car_version": "2023.44.30.8 06f534d46010",
|
||||||
|
"center_display_state": 0,
|
||||||
|
"dashcam_clip_save_available": true,
|
||||||
|
"dashcam_state": "Recording",
|
||||||
|
"df": 0,
|
||||||
|
"dr": 0,
|
||||||
|
"fd_window": 0,
|
||||||
|
"feature_bitmask": "fbdffbff,187f",
|
||||||
|
"fp_window": 0,
|
||||||
|
"ft": 0,
|
||||||
|
"is_user_present": false,
|
||||||
|
"locked": false,
|
||||||
|
"media_info": {
|
||||||
|
"a2dp_source_name": "Pixel 8 Pro",
|
||||||
|
"audio_volume": 1.6667,
|
||||||
|
"audio_volume_increment": 0.333333,
|
||||||
|
"audio_volume_max": 10.333333,
|
||||||
|
"media_playback_status": "Playing",
|
||||||
|
"now_playing_album": "Elon Musk",
|
||||||
|
"now_playing_artist": "Walter Isaacson",
|
||||||
|
"now_playing_duration": 651000,
|
||||||
|
"now_playing_elapsed": 1000,
|
||||||
|
"now_playing_source": "Audible",
|
||||||
|
"now_playing_station": "Elon Musk",
|
||||||
|
"now_playing_title": "Chapter 51: Cybertruck: Tesla, 2018–2019"
|
||||||
|
},
|
||||||
|
"media_state": {
|
||||||
|
"remote_control_enabled": true
|
||||||
|
},
|
||||||
|
"notifications_supported": true,
|
||||||
|
"odometer": 6481.019282,
|
||||||
|
"parsed_calendar_supported": true,
|
||||||
|
"pf": 0,
|
||||||
|
"pr": 0,
|
||||||
|
"rd_window": 0,
|
||||||
|
"remote_start": false,
|
||||||
|
"remote_start_enabled": true,
|
||||||
|
"remote_start_supported": true,
|
||||||
|
"rp_window": 0,
|
||||||
|
"rt": 0,
|
||||||
|
"santa_mode": 0,
|
||||||
|
"sentry_mode": false,
|
||||||
|
"sentry_mode_available": true,
|
||||||
|
"service_mode": false,
|
||||||
|
"service_mode_plus": false,
|
||||||
|
"software_update": {
|
||||||
|
"download_perc": 100,
|
||||||
|
"expected_duration_sec": 2700,
|
||||||
|
"install_perc": 1,
|
||||||
|
"status": "available",
|
||||||
|
"version": "2024.12.0.0"
|
||||||
|
},
|
||||||
|
"speed_limit_mode": {
|
||||||
|
"active": false,
|
||||||
|
"current_limit_mph": 69,
|
||||||
|
"max_limit_mph": 120,
|
||||||
|
"min_limit_mph": 50,
|
||||||
|
"pin_code_set": true
|
||||||
|
},
|
||||||
|
"sun_roof_state": "open",
|
||||||
|
"vehicle_state_sun_roof_percent_open": 20,
|
||||||
|
"timestamp": 1705707520649,
|
||||||
|
"tpms_hard_warning_fl": false,
|
||||||
|
"tpms_hard_warning_fr": false,
|
||||||
|
"tpms_hard_warning_rl": false,
|
||||||
|
"tpms_hard_warning_rr": false,
|
||||||
|
"tpms_last_seen_pressure_time_fl": 1705700812,
|
||||||
|
"tpms_last_seen_pressure_time_fr": 1705700793,
|
||||||
|
"tpms_last_seen_pressure_time_rl": 1705700794,
|
||||||
|
"tpms_last_seen_pressure_time_rr": 1705700823,
|
||||||
|
"tpms_pressure_fl": 2.775,
|
||||||
|
"tpms_pressure_fr": 2.8,
|
||||||
|
"tpms_pressure_rl": 2.775,
|
||||||
|
"tpms_pressure_rr": 2.775,
|
||||||
|
"tpms_rcp_front_value": 2.9,
|
||||||
|
"tpms_rcp_rear_value": 2.9,
|
||||||
|
"tpms_soft_warning_fl": false,
|
||||||
|
"tpms_soft_warning_fr": false,
|
||||||
|
"tpms_soft_warning_rl": false,
|
||||||
|
"tpms_soft_warning_rr": false,
|
||||||
|
"valet_mode": false,
|
||||||
|
"valet_pin_needed": false,
|
||||||
|
"vehicle_name": "Test",
|
||||||
|
"vehicle_self_test_progress": 0,
|
||||||
|
"vehicle_self_test_requested": false,
|
||||||
|
"webcam_available": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
279
tests/components/tesla_fleet/fixtures/vehicle_data_alt.json
Normal file
279
tests/components/tesla_fleet/fixtures/vehicle_data_alt.json
Normal file
|
@ -0,0 +1,279 @@
|
||||||
|
{
|
||||||
|
"response": {
|
||||||
|
"id": 1234,
|
||||||
|
"user_id": 1234,
|
||||||
|
"vehicle_id": 1234,
|
||||||
|
"vin": "LRWXF7EK4KC700000",
|
||||||
|
"color": null,
|
||||||
|
"access_type": "OWNER",
|
||||||
|
"granular_access": {
|
||||||
|
"hide_private": false
|
||||||
|
},
|
||||||
|
"tokens": ["abc", "def"],
|
||||||
|
"state": "online",
|
||||||
|
"in_service": false,
|
||||||
|
"id_s": "1234",
|
||||||
|
"calendar_enabled": true,
|
||||||
|
"api_version": 71,
|
||||||
|
"backseat_token": null,
|
||||||
|
"backseat_token_updated_at": null,
|
||||||
|
"ble_autopair_enrolled": false,
|
||||||
|
"charge_state": {
|
||||||
|
"battery_heater_on": true,
|
||||||
|
"battery_level": 77,
|
||||||
|
"battery_range": 266.87,
|
||||||
|
"charge_amps": 16,
|
||||||
|
"charge_current_request": 16,
|
||||||
|
"charge_current_request_max": 16,
|
||||||
|
"charge_enable_request": true,
|
||||||
|
"charge_energy_added": 0,
|
||||||
|
"charge_limit_soc": 80,
|
||||||
|
"charge_limit_soc_max": 100,
|
||||||
|
"charge_limit_soc_min": 50,
|
||||||
|
"charge_limit_soc_std": 80,
|
||||||
|
"charge_miles_added_ideal": 0,
|
||||||
|
"charge_miles_added_rated": 0,
|
||||||
|
"charge_port_cold_weather_mode": false,
|
||||||
|
"charge_port_color": "<invalid>",
|
||||||
|
"charge_port_door_open": true,
|
||||||
|
"charge_port_latch": "Engaged",
|
||||||
|
"charge_rate": 0,
|
||||||
|
"charger_actual_current": 0,
|
||||||
|
"charger_phases": null,
|
||||||
|
"charger_pilot_current": 16,
|
||||||
|
"charger_power": 0,
|
||||||
|
"charger_voltage": 2,
|
||||||
|
"charging_state": "Stopped",
|
||||||
|
"conn_charge_cable": "IEC",
|
||||||
|
"est_battery_range": 275.04,
|
||||||
|
"fast_charger_brand": "<invalid>",
|
||||||
|
"fast_charger_present": false,
|
||||||
|
"fast_charger_type": "ACSingleWireCAN",
|
||||||
|
"ideal_battery_range": 266.87,
|
||||||
|
"max_range_charge_counter": 0,
|
||||||
|
"minutes_to_full_charge": "bad value",
|
||||||
|
"not_enough_power_to_heat": null,
|
||||||
|
"off_peak_charging_enabled": false,
|
||||||
|
"off_peak_charging_times": "all_week",
|
||||||
|
"off_peak_hours_end_time": 900,
|
||||||
|
"preconditioning_enabled": false,
|
||||||
|
"preconditioning_times": "all_week",
|
||||||
|
"scheduled_charging_mode": "Off",
|
||||||
|
"scheduled_charging_pending": false,
|
||||||
|
"scheduled_charging_start_time": null,
|
||||||
|
"scheduled_charging_start_time_app": 600,
|
||||||
|
"scheduled_departure_time": 1704837600,
|
||||||
|
"scheduled_departure_time_minutes": 480,
|
||||||
|
"supercharger_session_trip_planner": false,
|
||||||
|
"time_to_full_charge": null,
|
||||||
|
"timestamp": null,
|
||||||
|
"trip_charging": false,
|
||||||
|
"usable_battery_level": 77,
|
||||||
|
"user_charge_enable_request": true
|
||||||
|
},
|
||||||
|
"climate_state": {
|
||||||
|
"allow_cabin_overheat_protection": true,
|
||||||
|
"auto_seat_climate_left": false,
|
||||||
|
"auto_seat_climate_right": false,
|
||||||
|
"auto_steering_wheel_heat": false,
|
||||||
|
"battery_heater": true,
|
||||||
|
"battery_heater_no_power": null,
|
||||||
|
"cabin_overheat_protection": "Off",
|
||||||
|
"cabin_overheat_protection_actively_cooling": false,
|
||||||
|
"climate_keeper_mode": "off",
|
||||||
|
"cop_activation_temperature": "Low",
|
||||||
|
"defrost_mode": 0,
|
||||||
|
"driver_temp_setting": 22,
|
||||||
|
"fan_status": 0,
|
||||||
|
"hvac_auto_request": "On",
|
||||||
|
"inside_temp": 29.8,
|
||||||
|
"is_auto_conditioning_on": false,
|
||||||
|
"is_climate_on": false,
|
||||||
|
"is_front_defroster_on": false,
|
||||||
|
"is_preconditioning": false,
|
||||||
|
"is_rear_defroster_on": false,
|
||||||
|
"left_temp_direction": 251,
|
||||||
|
"max_avail_temp": 28,
|
||||||
|
"min_avail_temp": 15,
|
||||||
|
"outside_temp": 30,
|
||||||
|
"passenger_temp_setting": 22,
|
||||||
|
"remote_heater_control_enabled": false,
|
||||||
|
"right_temp_direction": 251,
|
||||||
|
"seat_heater_left": 0,
|
||||||
|
"seat_heater_rear_center": 0,
|
||||||
|
"seat_heater_rear_left": 0,
|
||||||
|
"seat_heater_rear_right": 0,
|
||||||
|
"seat_heater_right": 0,
|
||||||
|
"side_mirror_heaters": false,
|
||||||
|
"steering_wheel_heat_level": 0,
|
||||||
|
"steering_wheel_heater": false,
|
||||||
|
"supports_fan_only_cabin_overheat_protection": true,
|
||||||
|
"timestamp": 1705707520649,
|
||||||
|
"wiper_blade_heater": false
|
||||||
|
},
|
||||||
|
"drive_state": {
|
||||||
|
"active_route_latitude": 30.2226265,
|
||||||
|
"active_route_longitude": -97.6236871,
|
||||||
|
"active_route_miles_to_arrival": 0,
|
||||||
|
"active_route_minutes_to_arrival": 0,
|
||||||
|
"active_route_traffic_minutes_delay": 0,
|
||||||
|
"gps_as_of": 1701129612,
|
||||||
|
"heading": 185,
|
||||||
|
"latitude": -30.222626,
|
||||||
|
"longitude": -97.6236871,
|
||||||
|
"native_latitude": -30.222626,
|
||||||
|
"native_location_supported": 1,
|
||||||
|
"native_longitude": -97.6236871,
|
||||||
|
"native_type": "wgs",
|
||||||
|
"power": -7,
|
||||||
|
"shift_state": null,
|
||||||
|
"speed": null,
|
||||||
|
"timestamp": 1705707520649
|
||||||
|
},
|
||||||
|
"gui_settings": {
|
||||||
|
"gui_24_hour_time": false,
|
||||||
|
"gui_charge_rate_units": "kW",
|
||||||
|
"gui_distance_units": "km/hr",
|
||||||
|
"gui_range_display": "Rated",
|
||||||
|
"gui_temperature_units": "C",
|
||||||
|
"gui_tirepressure_units": "Psi",
|
||||||
|
"show_range_units": false,
|
||||||
|
"timestamp": 1705707520649
|
||||||
|
},
|
||||||
|
"vehicle_config": {
|
||||||
|
"aux_park_lamps": "Eu",
|
||||||
|
"badge_version": 1,
|
||||||
|
"can_accept_navigation_requests": true,
|
||||||
|
"can_actuate_trunks": true,
|
||||||
|
"car_special_type": "base",
|
||||||
|
"car_type": "model3",
|
||||||
|
"charge_port_type": "CCS",
|
||||||
|
"cop_user_set_temp_supported": false,
|
||||||
|
"dashcam_clip_save_supported": true,
|
||||||
|
"default_charge_to_max": false,
|
||||||
|
"driver_assist": "TeslaAP3",
|
||||||
|
"ece_restrictions": false,
|
||||||
|
"efficiency_package": "M32021",
|
||||||
|
"eu_vehicle": true,
|
||||||
|
"exterior_color": "DeepBlue",
|
||||||
|
"exterior_trim": "Black",
|
||||||
|
"exterior_trim_override": "",
|
||||||
|
"has_air_suspension": false,
|
||||||
|
"has_ludicrous_mode": false,
|
||||||
|
"has_seat_cooling": false,
|
||||||
|
"headlamp_type": "Global",
|
||||||
|
"interior_trim_type": "White2",
|
||||||
|
"key_version": 2,
|
||||||
|
"motorized_charge_port": true,
|
||||||
|
"paint_color_override": "0,9,25,0.7,0.04",
|
||||||
|
"performance_package": "Base",
|
||||||
|
"plg": true,
|
||||||
|
"pws": true,
|
||||||
|
"rear_drive_unit": "PM216MOSFET",
|
||||||
|
"rear_seat_heaters": 1,
|
||||||
|
"rear_seat_type": 0,
|
||||||
|
"rhd": true,
|
||||||
|
"roof_color": "RoofColorGlass",
|
||||||
|
"seat_type": null,
|
||||||
|
"spoiler_type": "None",
|
||||||
|
"sun_roof_installed": null,
|
||||||
|
"supports_qr_pairing": false,
|
||||||
|
"third_row_seats": "None",
|
||||||
|
"timestamp": 1705707520649,
|
||||||
|
"trim_badging": "74d",
|
||||||
|
"use_range_badging": true,
|
||||||
|
"utc_offset": 36000,
|
||||||
|
"webcam_selfie_supported": true,
|
||||||
|
"webcam_supported": true,
|
||||||
|
"wheel_type": "Pinwheel18CapKit"
|
||||||
|
},
|
||||||
|
"vehicle_state": {
|
||||||
|
"api_version": 71,
|
||||||
|
"autopark_state_v2": "unavailable",
|
||||||
|
"calendar_supported": true,
|
||||||
|
"car_version": "2023.44.30.8 06f534d46010",
|
||||||
|
"center_display_state": 0,
|
||||||
|
"dashcam_clip_save_available": true,
|
||||||
|
"dashcam_state": "Recording",
|
||||||
|
"df": 0,
|
||||||
|
"dr": 0,
|
||||||
|
"fd_window": 1,
|
||||||
|
"feature_bitmask": "fbdffbff,187f",
|
||||||
|
"fp_window": 1,
|
||||||
|
"ft": 1,
|
||||||
|
"is_user_present": true,
|
||||||
|
"locked": false,
|
||||||
|
"media_info": {
|
||||||
|
"audio_volume": 2.6667,
|
||||||
|
"audio_volume_increment": 0.333333,
|
||||||
|
"audio_volume_max": 10.333333,
|
||||||
|
"media_playback_status": "Stopped",
|
||||||
|
"now_playing_album": "",
|
||||||
|
"now_playing_artist": "",
|
||||||
|
"now_playing_duration": 0,
|
||||||
|
"now_playing_elapsed": 0,
|
||||||
|
"now_playing_source": "Spotify",
|
||||||
|
"now_playing_station": "",
|
||||||
|
"now_playing_title": ""
|
||||||
|
},
|
||||||
|
"media_state": {
|
||||||
|
"remote_control_enabled": true
|
||||||
|
},
|
||||||
|
"notifications_supported": true,
|
||||||
|
"odometer": 6481.019282,
|
||||||
|
"parsed_calendar_supported": true,
|
||||||
|
"pf": 0,
|
||||||
|
"pr": 0,
|
||||||
|
"rd_window": 1,
|
||||||
|
"remote_start": false,
|
||||||
|
"remote_start_enabled": true,
|
||||||
|
"remote_start_supported": true,
|
||||||
|
"rp_window": 1,
|
||||||
|
"rt": 1,
|
||||||
|
"santa_mode": 0,
|
||||||
|
"sentry_mode": false,
|
||||||
|
"sentry_mode_available": true,
|
||||||
|
"service_mode": false,
|
||||||
|
"service_mode_plus": false,
|
||||||
|
"software_update": {
|
||||||
|
"download_perc": 0,
|
||||||
|
"expected_duration_sec": 2700,
|
||||||
|
"install_perc": 1,
|
||||||
|
"status": "",
|
||||||
|
"version": " "
|
||||||
|
},
|
||||||
|
"speed_limit_mode": {
|
||||||
|
"active": false,
|
||||||
|
"current_limit_mph": 69,
|
||||||
|
"max_limit_mph": 120,
|
||||||
|
"min_limit_mph": 50,
|
||||||
|
"pin_code_set": true
|
||||||
|
},
|
||||||
|
"timestamp": 1705707520649,
|
||||||
|
"tpms_hard_warning_fl": false,
|
||||||
|
"tpms_hard_warning_fr": false,
|
||||||
|
"tpms_hard_warning_rl": false,
|
||||||
|
"tpms_hard_warning_rr": false,
|
||||||
|
"tpms_last_seen_pressure_time_fl": 1705700812,
|
||||||
|
"tpms_last_seen_pressure_time_fr": 1705700793,
|
||||||
|
"tpms_last_seen_pressure_time_rl": 1705700794,
|
||||||
|
"tpms_last_seen_pressure_time_rr": 1705700823,
|
||||||
|
"tpms_pressure_fl": 2.775,
|
||||||
|
"tpms_pressure_fr": 2.8,
|
||||||
|
"tpms_pressure_rl": 2.775,
|
||||||
|
"tpms_pressure_rr": 2.775,
|
||||||
|
"tpms_rcp_front_value": 2.9,
|
||||||
|
"tpms_rcp_rear_value": 2.9,
|
||||||
|
"tpms_soft_warning_fl": false,
|
||||||
|
"tpms_soft_warning_fr": false,
|
||||||
|
"tpms_soft_warning_rl": false,
|
||||||
|
"tpms_soft_warning_rr": false,
|
||||||
|
"valet_mode": false,
|
||||||
|
"valet_pin_needed": false,
|
||||||
|
"vehicle_name": "Test",
|
||||||
|
"vehicle_self_test_progress": 0,
|
||||||
|
"vehicle_self_test_requested": false,
|
||||||
|
"webcam_available": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
129
tests/components/tesla_fleet/snapshots/test_init.ambr
Normal file
129
tests/components/tesla_fleet/snapshots/test_init.ambr
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
# serializer version: 1
|
||||||
|
# name: test_devices[{('tesla_fleet', '123456')}]
|
||||||
|
DeviceRegistryEntrySnapshot({
|
||||||
|
'area_id': None,
|
||||||
|
'config_entries': <ANY>,
|
||||||
|
'configuration_url': None,
|
||||||
|
'connections': set({
|
||||||
|
}),
|
||||||
|
'disabled_by': None,
|
||||||
|
'entry_type': None,
|
||||||
|
'hw_version': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'identifiers': set({
|
||||||
|
tuple(
|
||||||
|
'tesla_fleet',
|
||||||
|
'123456',
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
'is_new': False,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'manufacturer': 'Tesla',
|
||||||
|
'model': 'Powerwall 2, Tesla Backup Gateway 2',
|
||||||
|
'model_id': None,
|
||||||
|
'name': 'Energy Site',
|
||||||
|
'name_by_user': None,
|
||||||
|
'primary_config_entry': <ANY>,
|
||||||
|
'serial_number': '123456',
|
||||||
|
'suggested_area': None,
|
||||||
|
'sw_version': None,
|
||||||
|
'via_device_id': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_devices[{('tesla_fleet', 'LRWXF7EK4KC700000')}]
|
||||||
|
DeviceRegistryEntrySnapshot({
|
||||||
|
'area_id': None,
|
||||||
|
'config_entries': <ANY>,
|
||||||
|
'configuration_url': None,
|
||||||
|
'connections': set({
|
||||||
|
}),
|
||||||
|
'disabled_by': None,
|
||||||
|
'entry_type': None,
|
||||||
|
'hw_version': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'identifiers': set({
|
||||||
|
tuple(
|
||||||
|
'tesla_fleet',
|
||||||
|
'LRWXF7EK4KC700000',
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
'is_new': False,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'manufacturer': 'Tesla',
|
||||||
|
'model': 'Model X',
|
||||||
|
'model_id': None,
|
||||||
|
'name': 'Test',
|
||||||
|
'name_by_user': None,
|
||||||
|
'primary_config_entry': <ANY>,
|
||||||
|
'serial_number': 'LRWXF7EK4KC700000',
|
||||||
|
'suggested_area': None,
|
||||||
|
'sw_version': None,
|
||||||
|
'via_device_id': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_devices[{('tesla_fleet', 'abd-123')}]
|
||||||
|
DeviceRegistryEntrySnapshot({
|
||||||
|
'area_id': None,
|
||||||
|
'config_entries': <ANY>,
|
||||||
|
'configuration_url': None,
|
||||||
|
'connections': set({
|
||||||
|
}),
|
||||||
|
'disabled_by': None,
|
||||||
|
'entry_type': None,
|
||||||
|
'hw_version': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'identifiers': set({
|
||||||
|
tuple(
|
||||||
|
'tesla_fleet',
|
||||||
|
'abd-123',
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
'is_new': False,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'manufacturer': 'Tesla',
|
||||||
|
'model': 'Gen 3 Wall Connector',
|
||||||
|
'model_id': None,
|
||||||
|
'name': 'Wall Connector',
|
||||||
|
'name_by_user': None,
|
||||||
|
'primary_config_entry': <ANY>,
|
||||||
|
'serial_number': '123',
|
||||||
|
'suggested_area': None,
|
||||||
|
'sw_version': None,
|
||||||
|
'via_device_id': <ANY>,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_devices[{('tesla_fleet', 'bcd-234')}]
|
||||||
|
DeviceRegistryEntrySnapshot({
|
||||||
|
'area_id': None,
|
||||||
|
'config_entries': <ANY>,
|
||||||
|
'configuration_url': None,
|
||||||
|
'connections': set({
|
||||||
|
}),
|
||||||
|
'disabled_by': None,
|
||||||
|
'entry_type': None,
|
||||||
|
'hw_version': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'identifiers': set({
|
||||||
|
tuple(
|
||||||
|
'tesla_fleet',
|
||||||
|
'bcd-234',
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
'is_new': False,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'manufacturer': 'Tesla',
|
||||||
|
'model': 'Gen 3 Wall Connector',
|
||||||
|
'model_id': None,
|
||||||
|
'name': 'Wall Connector',
|
||||||
|
'name_by_user': None,
|
||||||
|
'primary_config_entry': <ANY>,
|
||||||
|
'serial_number': '234',
|
||||||
|
'suggested_area': None,
|
||||||
|
'sw_version': None,
|
||||||
|
'via_device_id': <ANY>,
|
||||||
|
})
|
||||||
|
# ---
|
3424
tests/components/tesla_fleet/snapshots/test_sensor.ambr
Normal file
3424
tests/components/tesla_fleet/snapshots/test_sensor.ambr
Normal file
File diff suppressed because it is too large
Load diff
224
tests/components/tesla_fleet/test_config_flow.py
Normal file
224
tests/components/tesla_fleet/test_config_flow.py
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
"""Test the Tesla Fleet config flow."""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.tesla_fleet.application_credentials import (
|
||||||
|
AUTHORIZE_URL,
|
||||||
|
CLIENT_ID,
|
||||||
|
TOKEN_URL,
|
||||||
|
)
|
||||||
|
from homeassistant.components.tesla_fleet.const import DOMAIN, SCOPES
|
||||||
|
from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
from homeassistant.helpers import config_entry_oauth2_flow
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||||
|
from tests.typing import ClientSessionGenerator
|
||||||
|
|
||||||
|
REDIRECT = "https://example.com/auth/external/callback"
|
||||||
|
UNIQUE_ID = "uid"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def access_token(hass: HomeAssistant) -> dict[str, str | list[str]]:
|
||||||
|
"""Return a valid access token."""
|
||||||
|
return config_entry_oauth2_flow._encode_jwt(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
"sub": UNIQUE_ID,
|
||||||
|
"aud": [],
|
||||||
|
"scp": [
|
||||||
|
"vehicle_device_data",
|
||||||
|
"vehicle_cmds",
|
||||||
|
"vehicle_charging_cmds",
|
||||||
|
"energy_device_data",
|
||||||
|
"energy_cmds",
|
||||||
|
"offline_access",
|
||||||
|
"openid",
|
||||||
|
],
|
||||||
|
"ou_code": "NA",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("current_request_with_host")
|
||||||
|
async def test_full_flow(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
|
aioclient_mock: AiohttpClientMocker,
|
||||||
|
access_token,
|
||||||
|
) -> None:
|
||||||
|
"""Check full flow."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
state = config_entry_oauth2_flow._encode_jwt(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
"flow_id": result["flow_id"],
|
||||||
|
"redirect_uri": REDIRECT,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.EXTERNAL_STEP
|
||||||
|
|
||||||
|
assert result["url"].startswith(AUTHORIZE_URL)
|
||||||
|
parsed_url = urlparse(result["url"])
|
||||||
|
parsed_query = parse_qs(parsed_url.query)
|
||||||
|
assert parsed_query["response_type"][0] == "code"
|
||||||
|
assert parsed_query["client_id"][0] == CLIENT_ID
|
||||||
|
assert parsed_query["redirect_uri"][0] == REDIRECT
|
||||||
|
assert parsed_query["state"][0] == state
|
||||||
|
assert parsed_query["scope"][0] == " ".join(SCOPES)
|
||||||
|
assert parsed_query["code_challenge"][0] is not None
|
||||||
|
|
||||||
|
client = await hass_client_no_auth()
|
||||||
|
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||||
|
assert resp.status == 200
|
||||||
|
assert resp.headers["content-type"] == "text/html; charset=utf-8"
|
||||||
|
|
||||||
|
aioclient_mock.clear_requests()
|
||||||
|
aioclient_mock.post(
|
||||||
|
TOKEN_URL,
|
||||||
|
json={
|
||||||
|
"refresh_token": "mock-refresh-token",
|
||||||
|
"access_token": access_token,
|
||||||
|
"type": "Bearer",
|
||||||
|
"expires_in": 60,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.tesla_fleet.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup:
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||||
|
|
||||||
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == UNIQUE_ID
|
||||||
|
assert "result" in result
|
||||||
|
assert result["result"].unique_id == UNIQUE_ID
|
||||||
|
assert "token" in result["result"].data
|
||||||
|
assert result["result"].data["token"]["access_token"] == access_token
|
||||||
|
assert result["result"].data["token"]["refresh_token"] == "mock-refresh-token"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("current_request_with_host")
|
||||||
|
async def test_reauthentication(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
|
aioclient_mock: AiohttpClientMocker,
|
||||||
|
access_token,
|
||||||
|
) -> None:
|
||||||
|
"""Test Tesla Fleet reauthentication."""
|
||||||
|
old_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
unique_id=UNIQUE_ID,
|
||||||
|
version=1,
|
||||||
|
data={},
|
||||||
|
)
|
||||||
|
old_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={
|
||||||
|
"source": SOURCE_REAUTH,
|
||||||
|
"unique_id": old_entry.unique_id,
|
||||||
|
"entry_id": old_entry.entry_id,
|
||||||
|
},
|
||||||
|
data=old_entry.data,
|
||||||
|
)
|
||||||
|
|
||||||
|
flows = hass.config_entries.flow.async_progress()
|
||||||
|
assert len(flows) == 1
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {})
|
||||||
|
|
||||||
|
state = config_entry_oauth2_flow._encode_jwt(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
"flow_id": result["flow_id"],
|
||||||
|
"redirect_uri": REDIRECT,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
client = await hass_client_no_auth()
|
||||||
|
await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||||
|
|
||||||
|
aioclient_mock.post(
|
||||||
|
TOKEN_URL,
|
||||||
|
json={
|
||||||
|
"refresh_token": "mock-refresh-token",
|
||||||
|
"access_token": access_token,
|
||||||
|
"type": "Bearer",
|
||||||
|
"expires_in": 60,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.tesla_fleet.async_setup_entry", return_value=True
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||||
|
|
||||||
|
assert result
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "reauth_successful"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("current_request_with_host")
|
||||||
|
async def test_reauth_account_mismatch(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
|
aioclient_mock: AiohttpClientMocker,
|
||||||
|
access_token,
|
||||||
|
) -> None:
|
||||||
|
"""Test Tesla Fleet reauthentication with different account."""
|
||||||
|
old_entry = MockConfigEntry(domain=DOMAIN, unique_id="baduid", version=1, data={})
|
||||||
|
old_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={
|
||||||
|
"source": SOURCE_REAUTH,
|
||||||
|
"unique_id": old_entry.unique_id,
|
||||||
|
"entry_id": old_entry.entry_id,
|
||||||
|
},
|
||||||
|
data=old_entry.data,
|
||||||
|
)
|
||||||
|
|
||||||
|
flows = hass.config_entries.flow.async_progress()
|
||||||
|
result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {})
|
||||||
|
|
||||||
|
state = config_entry_oauth2_flow._encode_jwt(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
"flow_id": result["flow_id"],
|
||||||
|
"redirect_uri": REDIRECT,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
client = await hass_client_no_auth()
|
||||||
|
await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||||
|
|
||||||
|
aioclient_mock.post(
|
||||||
|
TOKEN_URL,
|
||||||
|
json={
|
||||||
|
"refresh_token": "mock-refresh-token",
|
||||||
|
"access_token": access_token,
|
||||||
|
"type": "Bearer",
|
||||||
|
"expires_in": 60,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.tesla_fleet.async_setup_entry", return_value=True
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "reauth_account_mismatch"
|
327
tests/components/tesla_fleet/test_init.py
Normal file
327
tests/components/tesla_fleet/test_init.py
Normal file
|
@ -0,0 +1,327 @@
|
||||||
|
"""Test the Tesla Fleet init."""
|
||||||
|
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
|
import pytest
|
||||||
|
from syrupy import SnapshotAssertion
|
||||||
|
from tesla_fleet_api.exceptions import (
|
||||||
|
InvalidToken,
|
||||||
|
LoginRequired,
|
||||||
|
OAuthExpired,
|
||||||
|
RateLimited,
|
||||||
|
TeslaFleetError,
|
||||||
|
VehicleOffline,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.components.tesla_fleet.coordinator import (
|
||||||
|
ENERGY_INTERVAL,
|
||||||
|
ENERGY_INTERVAL_SECONDS,
|
||||||
|
VEHICLE_INTERVAL,
|
||||||
|
VEHICLE_INTERVAL_SECONDS,
|
||||||
|
VEHICLE_WAIT,
|
||||||
|
)
|
||||||
|
from homeassistant.components.tesla_fleet.models import TeslaFleetData
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
|
||||||
|
from . import setup_platform
|
||||||
|
from .const import VEHICLE_ASLEEP, VEHICLE_DATA_ALT
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
|
||||||
|
ERRORS = [
|
||||||
|
(InvalidToken, ConfigEntryState.SETUP_ERROR),
|
||||||
|
(OAuthExpired, ConfigEntryState.SETUP_ERROR),
|
||||||
|
(LoginRequired, ConfigEntryState.SETUP_ERROR),
|
||||||
|
(TeslaFleetError, ConfigEntryState.SETUP_RETRY),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_load_unload(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
normal_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test load and unload."""
|
||||||
|
|
||||||
|
await setup_platform(hass, normal_config_entry)
|
||||||
|
|
||||||
|
assert normal_config_entry.state is ConfigEntryState.LOADED
|
||||||
|
assert isinstance(normal_config_entry.runtime_data, TeslaFleetData)
|
||||||
|
assert await hass.config_entries.async_unload(normal_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert normal_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||||
|
assert not hasattr(normal_config_entry, "runtime_data")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("side_effect", "state"), ERRORS)
|
||||||
|
async def test_init_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
normal_config_entry: MockConfigEntry,
|
||||||
|
mock_products,
|
||||||
|
side_effect,
|
||||||
|
state,
|
||||||
|
) -> None:
|
||||||
|
"""Test init with errors."""
|
||||||
|
|
||||||
|
mock_products.side_effect = side_effect
|
||||||
|
await setup_platform(hass, normal_config_entry)
|
||||||
|
assert normal_config_entry.state is state
|
||||||
|
|
||||||
|
|
||||||
|
# Test devices
|
||||||
|
async def test_devices(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
normal_config_entry: MockConfigEntry,
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test device registry."""
|
||||||
|
await setup_platform(hass, normal_config_entry)
|
||||||
|
devices = dr.async_entries_for_config_entry(
|
||||||
|
device_registry, normal_config_entry.entry_id
|
||||||
|
)
|
||||||
|
|
||||||
|
for device in devices:
|
||||||
|
assert device == snapshot(name=f"{device.identifiers}")
|
||||||
|
|
||||||
|
|
||||||
|
# Vehicle Coordinator
|
||||||
|
async def test_vehicle_refresh_offline(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
normal_config_entry: MockConfigEntry,
|
||||||
|
mock_vehicle_state,
|
||||||
|
mock_vehicle_data,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test coordinator refresh with an error."""
|
||||||
|
await setup_platform(hass, normal_config_entry)
|
||||||
|
assert normal_config_entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
mock_vehicle_state.assert_called_once()
|
||||||
|
mock_vehicle_data.assert_called_once()
|
||||||
|
mock_vehicle_state.reset_mock()
|
||||||
|
mock_vehicle_data.reset_mock()
|
||||||
|
|
||||||
|
# Test the unlikely condition that a vehicle state is online but actually offline
|
||||||
|
mock_vehicle_data.side_effect = VehicleOffline
|
||||||
|
freezer.tick(VEHICLE_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
mock_vehicle_state.assert_called_once()
|
||||||
|
mock_vehicle_data.assert_called_once()
|
||||||
|
mock_vehicle_state.reset_mock()
|
||||||
|
mock_vehicle_data.reset_mock()
|
||||||
|
|
||||||
|
# Test the normal condition that a vehcile state is offline
|
||||||
|
mock_vehicle_state.return_value = VEHICLE_ASLEEP
|
||||||
|
freezer.tick(VEHICLE_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
mock_vehicle_state.assert_called_once()
|
||||||
|
mock_vehicle_data.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("side_effect"), ERRORS)
|
||||||
|
async def test_vehicle_refresh_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
normal_config_entry: MockConfigEntry,
|
||||||
|
mock_vehicle_state,
|
||||||
|
side_effect,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test coordinator refresh makes entity unavailable."""
|
||||||
|
|
||||||
|
await setup_platform(hass, normal_config_entry)
|
||||||
|
|
||||||
|
mock_vehicle_state.side_effect = side_effect
|
||||||
|
freezer.tick(VEHICLE_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert (state := hass.states.get("sensor.test_battery_level"))
|
||||||
|
assert state.state == "unavailable"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_vehicle_refresh_ratelimited(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
normal_config_entry: MockConfigEntry,
|
||||||
|
mock_vehicle_data,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test coordinator refresh handles 429."""
|
||||||
|
|
||||||
|
mock_vehicle_data.side_effect = RateLimited(
|
||||||
|
{"after": VEHICLE_INTERVAL_SECONDS + 10}
|
||||||
|
)
|
||||||
|
await setup_platform(hass, normal_config_entry)
|
||||||
|
|
||||||
|
assert (state := hass.states.get("sensor.test_battery_level"))
|
||||||
|
assert state.state == "unknown"
|
||||||
|
assert mock_vehicle_data.call_count == 1
|
||||||
|
|
||||||
|
freezer.tick(VEHICLE_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Should not call for another 10 seconds
|
||||||
|
assert mock_vehicle_data.call_count == 1
|
||||||
|
|
||||||
|
freezer.tick(VEHICLE_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_vehicle_data.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_vehicle_sleep(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
normal_config_entry: MockConfigEntry,
|
||||||
|
mock_vehicle_data,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test coordinator refresh with an error."""
|
||||||
|
await setup_platform(hass, normal_config_entry)
|
||||||
|
assert mock_vehicle_data.call_count == 1
|
||||||
|
|
||||||
|
freezer.tick(VEHICLE_WAIT + VEHICLE_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
# Let vehicle sleep, no updates for 15 minutes
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_vehicle_data.call_count == 2
|
||||||
|
|
||||||
|
freezer.tick(VEHICLE_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
# No polling, call_count should not increase
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_vehicle_data.call_count == 2
|
||||||
|
|
||||||
|
freezer.tick(VEHICLE_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
# No polling, call_count should not increase
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_vehicle_data.call_count == 2
|
||||||
|
|
||||||
|
freezer.tick(VEHICLE_WAIT)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
# Vehicle didn't sleep, go back to normal
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_vehicle_data.call_count == 3
|
||||||
|
|
||||||
|
freezer.tick(VEHICLE_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
# Regular polling
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_vehicle_data.call_count == 4
|
||||||
|
|
||||||
|
mock_vehicle_data.return_value = VEHICLE_DATA_ALT
|
||||||
|
freezer.tick(VEHICLE_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
# Vehicle active
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_vehicle_data.call_count == 5
|
||||||
|
|
||||||
|
freezer.tick(VEHICLE_WAIT)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
# Dont let sleep when active
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_vehicle_data.call_count == 6
|
||||||
|
|
||||||
|
freezer.tick(VEHICLE_WAIT)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
# Dont let sleep when active
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_vehicle_data.call_count == 7
|
||||||
|
|
||||||
|
|
||||||
|
# Test Energy Live Coordinator
|
||||||
|
@pytest.mark.parametrize(("side_effect", "state"), ERRORS)
|
||||||
|
async def test_energy_live_refresh_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
normal_config_entry: MockConfigEntry,
|
||||||
|
mock_live_status,
|
||||||
|
side_effect,
|
||||||
|
state,
|
||||||
|
) -> None:
|
||||||
|
"""Test coordinator refresh with an error."""
|
||||||
|
mock_live_status.side_effect = side_effect
|
||||||
|
await setup_platform(hass, normal_config_entry)
|
||||||
|
assert normal_config_entry.state is state
|
||||||
|
|
||||||
|
|
||||||
|
# Test Energy Site Coordinator
|
||||||
|
@pytest.mark.parametrize(("side_effect", "state"), ERRORS)
|
||||||
|
async def test_energy_site_refresh_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
normal_config_entry: MockConfigEntry,
|
||||||
|
mock_site_info,
|
||||||
|
side_effect,
|
||||||
|
state,
|
||||||
|
) -> None:
|
||||||
|
"""Test coordinator refresh with an error."""
|
||||||
|
mock_site_info.side_effect = side_effect
|
||||||
|
await setup_platform(hass, normal_config_entry)
|
||||||
|
assert normal_config_entry.state is state
|
||||||
|
|
||||||
|
|
||||||
|
async def test_energy_live_refresh_ratelimited(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
normal_config_entry: MockConfigEntry,
|
||||||
|
mock_live_status,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test coordinator refresh handles 429."""
|
||||||
|
|
||||||
|
await setup_platform(hass, normal_config_entry)
|
||||||
|
|
||||||
|
mock_live_status.side_effect = RateLimited({"after": ENERGY_INTERVAL_SECONDS + 10})
|
||||||
|
freezer.tick(ENERGY_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_live_status.call_count == 2
|
||||||
|
|
||||||
|
freezer.tick(ENERGY_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Should not call for another 10 seconds
|
||||||
|
assert mock_live_status.call_count == 2
|
||||||
|
|
||||||
|
freezer.tick(ENERGY_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_live_status.call_count == 3
|
||||||
|
|
||||||
|
|
||||||
|
async def test_energy_info_refresh_ratelimited(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
normal_config_entry: MockConfigEntry,
|
||||||
|
mock_site_info,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test coordinator refresh handles 429."""
|
||||||
|
|
||||||
|
await setup_platform(hass, normal_config_entry)
|
||||||
|
|
||||||
|
mock_site_info.side_effect = RateLimited({"after": ENERGY_INTERVAL_SECONDS + 10})
|
||||||
|
freezer.tick(ENERGY_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_site_info.call_count == 2
|
||||||
|
|
||||||
|
freezer.tick(ENERGY_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Should not call for another 10 seconds
|
||||||
|
assert mock_site_info.call_count == 2
|
||||||
|
|
||||||
|
freezer.tick(ENERGY_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_site_info.call_count == 3
|
41
tests/components/tesla_fleet/test_sensor.py
Normal file
41
tests/components/tesla_fleet/test_sensor.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
"""Test the Tesla Fleet sensor platform."""
|
||||||
|
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
|
import pytest
|
||||||
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.components.tesla_fleet.coordinator import VEHICLE_INTERVAL
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from . import assert_entities, assert_entities_alt, setup_platform
|
||||||
|
from .const import VEHICLE_DATA_ALT
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_sensors(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
normal_config_entry: MockConfigEntry,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
mock_vehicle_data,
|
||||||
|
) -> None:
|
||||||
|
"""Tests that the sensor entities are correct."""
|
||||||
|
|
||||||
|
freezer.move_to("2024-01-01 00:00:00+00:00")
|
||||||
|
|
||||||
|
await setup_platform(hass, normal_config_entry, [Platform.SENSOR])
|
||||||
|
|
||||||
|
assert_entities(hass, normal_config_entry.entry_id, entity_registry, snapshot)
|
||||||
|
|
||||||
|
# Coordinator refresh
|
||||||
|
mock_vehicle_data.return_value = VEHICLE_DATA_ALT
|
||||||
|
freezer.tick(VEHICLE_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert_entities_alt(hass, normal_config_entry.entry_id, entity_registry, snapshot)
|
Loading…
Add table
Reference in a new issue