diff --git a/CODEOWNERS b/CODEOWNERS index 2c572aaa762..177fd129699 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1432,6 +1432,8 @@ build.json @home-assistant/supervisor /tests/components/tellduslive/ @fredrike /homeassistant/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 /tests/components/tesla_wall_connector/ @einarhauks /homeassistant/components/teslemetry/ @Bre77 diff --git a/homeassistant/brands/tesla.json b/homeassistant/brands/tesla.json index aeec7982579..13b3b448cdd 100644 --- a/homeassistant/brands/tesla.json +++ b/homeassistant/brands/tesla.json @@ -1,5 +1,5 @@ { "domain": "tesla", "name": "Tesla", - "integrations": ["powerwall", "tesla_wall_connector"] + "integrations": ["powerwall", "tesla_wall_connector", "tesla_fleet"] } diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py new file mode 100644 index 00000000000..d43d3d51a41 --- /dev/null +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -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) diff --git a/homeassistant/components/tesla_fleet/application_credentials.py b/homeassistant/components/tesla_fleet/application_credentials.py new file mode 100644 index 00000000000..fda9fce8cec --- /dev/null +++ b/homeassistant/components/tesla_fleet/application_credentials.py @@ -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 + } + ) diff --git a/homeassistant/components/tesla_fleet/config_flow.py b/homeassistant/components/tesla_fleet/config_flow.py new file mode 100644 index 00000000000..ad6ba8817c9 --- /dev/null +++ b/homeassistant/components/tesla_fleet/config_flow.py @@ -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() diff --git a/homeassistant/components/tesla_fleet/const.py b/homeassistant/components/tesla_fleet/const.py new file mode 100644 index 00000000000..ae622d2266c --- /dev/null +++ b/homeassistant/components/tesla_fleet/const.py @@ -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" diff --git a/homeassistant/components/tesla_fleet/coordinator.py b/homeassistant/components/tesla_fleet/coordinator.py new file mode 100644 index 00000000000..dad592d3033 --- /dev/null +++ b/homeassistant/components/tesla_fleet/coordinator.py @@ -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) diff --git a/homeassistant/components/tesla_fleet/entity.py b/homeassistant/components/tesla_fleet/entity.py new file mode 100644 index 00000000000..c853bb798b5 --- /dev/null +++ b/homeassistant/components/tesla_fleet/entity.py @@ -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) + ) diff --git a/homeassistant/components/tesla_fleet/icons.json b/homeassistant/components/tesla_fleet/icons.json new file mode 100644 index 00000000000..2180611ea94 --- /dev/null +++ b/homeassistant/components/tesla_fleet/icons.json @@ -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" + } + } + } +} diff --git a/homeassistant/components/tesla_fleet/manifest.json b/homeassistant/components/tesla_fleet/manifest.json new file mode 100644 index 00000000000..310d8940432 --- /dev/null +++ b/homeassistant/components/tesla_fleet/manifest.json @@ -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"] +} diff --git a/homeassistant/components/tesla_fleet/models.py b/homeassistant/components/tesla_fleet/models.py new file mode 100644 index 00000000000..1b1f5f083cd --- /dev/null +++ b/homeassistant/components/tesla_fleet/models.py @@ -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 diff --git a/homeassistant/components/tesla_fleet/sensor.py b/homeassistant/components/tesla_fleet/sensor.py new file mode 100644 index 00000000000..4d30a509e1a --- /dev/null +++ b/homeassistant/components/tesla_fleet/sensor.py @@ -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 diff --git a/homeassistant/components/tesla_fleet/strings.json b/homeassistant/components/tesla_fleet/strings.json new file mode 100644 index 00000000000..4a571ae0a2e --- /dev/null +++ b/homeassistant/components/tesla_fleet/strings.json @@ -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}" + } + } +} diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index c576f242e30..879fd6f4839 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -24,6 +24,7 @@ APPLICATION_CREDENTIALS = [ "netatmo", "senz", "spotify", + "tesla_fleet", "twitch", "withings", "xbox", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 0a1b5e96516..62980e3179d 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -565,6 +565,7 @@ FLOWS = { "technove", "tedee", "tellduslive", + "tesla_fleet", "tesla_wall_connector", "teslemetry", "tessie", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 1b23c86fcd8..2d6cb4d243f 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6121,6 +6121,12 @@ "config_flow": true, "iot_class": "local_polling", "name": "Tesla Wall Connector" + }, + "tesla_fleet": { + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Tesla Fleet" } } }, diff --git a/requirements_all.txt b/requirements_all.txt index 47c46a3beda..039cb624ef0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2711,6 +2711,7 @@ temperusb==1.6.1 # homeassistant.components.tensorflow # tensorflow==2.5.0 +# homeassistant.components.tesla_fleet # homeassistant.components.teslemetry # homeassistant.components.tessie tesla-fleet-api==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5f80da6bf68..c1f06afb3ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2121,6 +2121,7 @@ temescal==0.5 # homeassistant.components.temper temperusb==1.6.1 +# homeassistant.components.tesla_fleet # homeassistant.components.teslemetry # homeassistant.components.tessie tesla-fleet-api==0.7.2 diff --git a/tests/components/tesla_fleet/__init__.py b/tests/components/tesla_fleet/__init__.py new file mode 100644 index 00000000000..d5df0d0a2ed --- /dev/null +++ b/tests/components/tesla_fleet/__init__.py @@ -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") diff --git a/tests/components/tesla_fleet/conftest.py b/tests/components/tesla_fleet/conftest.py new file mode 100644 index 00000000000..4e4d7b406d5 --- /dev/null +++ b/tests/components/tesla_fleet/conftest.py @@ -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 diff --git a/tests/components/tesla_fleet/const.py b/tests/components/tesla_fleet/const.py new file mode 100644 index 00000000000..76b4ae20092 --- /dev/null +++ b/tests/components/tesla_fleet/const.py @@ -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} diff --git a/tests/components/tesla_fleet/fixtures/live_status.json b/tests/components/tesla_fleet/fixtures/live_status.json new file mode 100644 index 00000000000..486f9f4fadd --- /dev/null +++ b/tests/components/tesla_fleet/fixtures/live_status.json @@ -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 + } + ] + } +} diff --git a/tests/components/tesla_fleet/fixtures/products.json b/tests/components/tesla_fleet/fixtures/products.json new file mode 100644 index 00000000000..e1b76e4cefb --- /dev/null +++ b/tests/components/tesla_fleet/fixtures/products.json @@ -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 +} diff --git a/tests/components/tesla_fleet/fixtures/site_info.json b/tests/components/tesla_fleet/fixtures/site_info.json new file mode 100644 index 00000000000..60958bbabbb --- /dev/null +++ b/tests/components/tesla_fleet/fixtures/site_info.json @@ -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 + } +} diff --git a/tests/components/tesla_fleet/fixtures/vehicle_data.json b/tests/components/tesla_fleet/fixtures/vehicle_data.json new file mode 100644 index 00000000000..3845ae48559 --- /dev/null +++ b/tests/components/tesla_fleet/fixtures/vehicle_data.json @@ -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": "", + "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": "", + "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 + } + } +} diff --git a/tests/components/tesla_fleet/fixtures/vehicle_data_alt.json b/tests/components/tesla_fleet/fixtures/vehicle_data_alt.json new file mode 100644 index 00000000000..76416982eba --- /dev/null +++ b/tests/components/tesla_fleet/fixtures/vehicle_data_alt.json @@ -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": "", + "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": "", + "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 + } + } +} diff --git a/tests/components/tesla_fleet/snapshots/test_init.ambr b/tests/components/tesla_fleet/snapshots/test_init.ambr new file mode 100644 index 00000000000..e9828db9f1b --- /dev/null +++ b/tests/components/tesla_fleet/snapshots/test_init.ambr @@ -0,0 +1,129 @@ +# serializer version: 1 +# name: test_devices[{('tesla_fleet', '123456')}] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + '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': , + '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': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + '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': , + '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': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + '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': , + 'serial_number': '123', + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': , + }) +# --- +# name: test_devices[{('tesla_fleet', 'bcd-234')}] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + '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': , + 'serial_number': '234', + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': , + }) +# --- diff --git a/tests/components/tesla_fleet/snapshots/test_sensor.ambr b/tests/components/tesla_fleet/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..e4c4c3d96c2 --- /dev/null +++ b/tests/components/tesla_fleet/snapshots/test_sensor.ambr @@ -0,0 +1,3424 @@ +# serializer version: 1 +# name: test_sensors[sensor.energy_site_battery_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_battery_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery power', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_power', + 'unique_id': '123456-battery_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_battery_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Battery power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_battery_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.06', + }) +# --- +# name: test_sensors[sensor.energy_site_battery_power-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Battery power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_battery_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.06', + }) +# --- +# name: test_sensors[sensor.energy_site_energy_left-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.energy_site_energy_left', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy left', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_left', + 'unique_id': '123456-energy_left', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_energy_left-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy_storage', + 'friendly_name': 'Energy Site Energy left', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_energy_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '38.8964736842105', + }) +# --- +# name: test_sensors[sensor.energy_site_energy_left-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy_storage', + 'friendly_name': 'Energy Site Energy left', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_energy_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '38.8964736842105', + }) +# --- +# name: test_sensors[sensor.energy_site_generator_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_generator_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Generator power', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'generator_power', + 'unique_id': '123456-generator_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_generator_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Generator power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_generator_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_generator_power-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Generator power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_generator_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_grid_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid power', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grid_power', + 'unique_id': '123456-grid_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_grid_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Grid power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_power-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Grid power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_services_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_grid_services_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid services power', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grid_services_power', + 'unique_id': '123456-grid_services_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_grid_services_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Grid services power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_services_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_services_power-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Grid services power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_services_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_load_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_load_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Load power', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'load_power', + 'unique_id': '123456-load_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_load_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Load power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_load_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.245', + }) +# --- +# name: test_sensors[sensor.energy_site_load_power-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Load power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_load_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.245', + }) +# --- +# name: test_sensors[sensor.energy_site_none-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_none', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'island_status', + 'unique_id': '123456-island_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.energy_site_none-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Energy Site None', + }), + 'context': , + 'entity_id': 'sensor.energy_site_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on_grid', + }) +# --- +# name: test_sensors[sensor.energy_site_none-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Energy Site None', + }), + 'context': , + 'entity_id': 'sensor.energy_site_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on_grid', + }) +# --- +# name: test_sensors[sensor.energy_site_percentage_charged-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_percentage_charged', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Percentage charged', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'percentage_charged', + 'unique_id': '123456-percentage_charged', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.energy_site_percentage_charged-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Energy Site Percentage charged', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.energy_site_percentage_charged', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '95.5053740373966', + }) +# --- +# name: test_sensors[sensor.energy_site_percentage_charged-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Energy Site Percentage charged', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.energy_site_percentage_charged', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '95.5053740373966', + }) +# --- +# name: test_sensors[sensor.energy_site_solar_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_solar_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Solar power', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_power', + 'unique_id': '123456-solar_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_solar_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Solar power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_solar_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.185', + }) +# --- +# name: test_sensors[sensor.energy_site_solar_power-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Solar power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_solar_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.185', + }) +# --- +# name: test_sensors[sensor.energy_site_total_pack_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.energy_site_total_pack_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total pack energy', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_pack_energy', + 'unique_id': '123456-total_pack_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_total_pack_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy_storage', + 'friendly_name': 'Energy Site Total pack energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_total_pack_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.727', + }) +# --- +# name: test_sensors[sensor.energy_site_total_pack_energy-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy_storage', + 'friendly_name': 'Energy Site Total pack energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_total_pack_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.727', + }) +# --- +# name: test_sensors[sensor.energy_site_version-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_version', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'version', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'version', + 'unique_id': '123456-version', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.energy_site_version-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Energy Site version', + }), + 'context': , + 'entity_id': 'sensor.energy_site_version', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '23.44.0 eb113390', + }) +# --- +# name: test_sensors[sensor.energy_site_version-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Energy Site version', + }), + 'context': , + 'entity_id': 'sensor.energy_site_version', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '23.44.0 eb113390', + }) +# --- +# name: test_sensors[sensor.energy_site_vpp_backup_reserve-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.energy_site_vpp_backup_reserve', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'VPP backup reserve', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vpp_backup_reserve_percent', + 'unique_id': '123456-vpp_backup_reserve_percent', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.energy_site_vpp_backup_reserve-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Energy Site VPP backup reserve', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.energy_site_vpp_backup_reserve', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[sensor.energy_site_vpp_backup_reserve-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Energy Site VPP backup reserve', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.energy_site_vpp_backup_reserve', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[sensor.test_battery_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_battery_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery level', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_battery_level', + 'unique_id': 'LRWXF7EK4KC700000-charge_state_battery_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.test_battery_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Test Battery level', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.test_battery_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '77', + }) +# --- +# name: test_sensors[sensor.test_battery_level-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Test Battery level', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.test_battery_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '77', + }) +# --- +# name: test_sensors[sensor.test_battery_range-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_battery_range', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery range', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_battery_range', + 'unique_id': 'LRWXF7EK4KC700000-charge_state_battery_range', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.test_battery_range-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Test Battery range', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_battery_range', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '429.48563328', + }) +# --- +# name: test_sensors[sensor.test_battery_range-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Test Battery range', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_battery_range', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '429.48563328', + }) +# --- +# name: test_sensors[sensor.test_charge_cable-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_charge_cable', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charge cable', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_conn_charge_cable', + 'unique_id': 'LRWXF7EK4KC700000-charge_state_conn_charge_cable', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_charge_cable-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Charge cable', + }), + 'context': , + 'entity_id': 'sensor.test_charge_cable', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'IEC', + }) +# --- +# name: test_sensors[sensor.test_charge_cable-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Charge cable', + }), + 'context': , + 'entity_id': 'sensor.test_charge_cable', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'IEC', + }) +# --- +# name: test_sensors[sensor.test_charge_energy_added-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_charge_energy_added', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charge energy added', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_charge_energy_added', + 'unique_id': 'LRWXF7EK4KC700000-charge_state_charge_energy_added', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.test_charge_energy_added-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Test Charge energy added', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_charge_energy_added', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[sensor.test_charge_energy_added-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Test Charge energy added', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_charge_energy_added', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[sensor.test_charge_rate-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_charge_rate', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charge rate', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_charge_rate', + 'unique_id': 'LRWXF7EK4KC700000-charge_state_charge_rate', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.test_charge_rate-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'speed', + 'friendly_name': 'Test Charge rate', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_charge_rate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[sensor.test_charge_rate-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'speed', + 'friendly_name': 'Test Charge rate', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_charge_rate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[sensor.test_charger_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_charger_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charger current', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_charger_actual_current', + 'unique_id': 'LRWXF7EK4KC700000-charge_state_charger_actual_current', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.test_charger_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Test Charger current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_charger_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[sensor.test_charger_current-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Test Charger current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_charger_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[sensor.test_charger_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_charger_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charger power', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_charger_power', + 'unique_id': 'LRWXF7EK4KC700000-charge_state_charger_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.test_charger_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Test Charger power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_charger_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[sensor.test_charger_power-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Test Charger power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_charger_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[sensor.test_charger_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_charger_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charger voltage', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_charger_voltage', + 'unique_id': 'LRWXF7EK4KC700000-charge_state_charger_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.test_charger_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Test Charger voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_charger_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_sensors[sensor.test_charger_voltage-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Test Charger voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_charger_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_sensors[sensor.test_charging-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'starting', + 'charging', + 'stopped', + 'complete', + 'disconnected', + 'no_power', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_charging', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_charging_state', + 'unique_id': 'LRWXF7EK4KC700000-charge_state_charging_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_charging-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Test Charging', + 'options': list([ + 'starting', + 'charging', + 'stopped', + 'complete', + 'disconnected', + 'no_power', + ]), + }), + 'context': , + 'entity_id': 'sensor.test_charging', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'stopped', + }) +# --- +# name: test_sensors[sensor.test_charging-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Test Charging', + 'options': list([ + 'starting', + 'charging', + 'stopped', + 'complete', + 'disconnected', + 'no_power', + ]), + }), + 'context': , + 'entity_id': 'sensor.test_charging', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'stopped', + }) +# --- +# name: test_sensors[sensor.test_distance_to_arrival-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_distance_to_arrival', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Distance to arrival', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drive_state_active_route_miles_to_arrival', + 'unique_id': 'LRWXF7EK4KC700000-drive_state_active_route_miles_to_arrival', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.test_distance_to_arrival-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Test Distance to arrival', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_distance_to_arrival', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.063555', + }) +# --- +# name: test_sensors[sensor.test_distance_to_arrival-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Test Distance to arrival', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_distance_to_arrival', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[sensor.test_driver_temperature_setting-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_driver_temperature_setting', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Driver temperature setting', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'climate_state_driver_temp_setting', + 'unique_id': 'LRWXF7EK4KC700000-climate_state_driver_temp_setting', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.test_driver_temperature_setting-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Test Driver temperature setting', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_driver_temperature_setting', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22', + }) +# --- +# name: test_sensors[sensor.test_driver_temperature_setting-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Test Driver temperature setting', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_driver_temperature_setting', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22', + }) +# --- +# name: test_sensors[sensor.test_estimate_battery_range-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_estimate_battery_range', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Estimate battery range', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_est_battery_range', + 'unique_id': 'LRWXF7EK4KC700000-charge_state_est_battery_range', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.test_estimate_battery_range-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Test Estimate battery range', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_estimate_battery_range', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '442.63397376', + }) +# --- +# name: test_sensors[sensor.test_estimate_battery_range-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Test Estimate battery range', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_estimate_battery_range', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '442.63397376', + }) +# --- +# name: test_sensors[sensor.test_fast_charger_type-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_fast_charger_type', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Fast charger type', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_fast_charger_type', + 'unique_id': 'LRWXF7EK4KC700000-charge_state_fast_charger_type', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_fast_charger_type-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Fast charger type', + }), + 'context': , + 'entity_id': 'sensor.test_fast_charger_type', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ACSingleWireCAN', + }) +# --- +# name: test_sensors[sensor.test_fast_charger_type-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Fast charger type', + }), + 'context': , + 'entity_id': 'sensor.test_fast_charger_type', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ACSingleWireCAN', + }) +# --- +# name: test_sensors[sensor.test_ideal_battery_range-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_ideal_battery_range', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Ideal battery range', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_ideal_battery_range', + 'unique_id': 'LRWXF7EK4KC700000-charge_state_ideal_battery_range', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.test_ideal_battery_range-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Test Ideal battery range', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_ideal_battery_range', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '429.48563328', + }) +# --- +# name: test_sensors[sensor.test_ideal_battery_range-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Test Ideal battery range', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_ideal_battery_range', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '429.48563328', + }) +# --- +# name: test_sensors[sensor.test_inside_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_inside_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Inside temperature', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'climate_state_inside_temp', + 'unique_id': 'LRWXF7EK4KC700000-climate_state_inside_temp', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.test_inside_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Test Inside temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_inside_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '29.8', + }) +# --- +# name: test_sensors[sensor.test_inside_temperature-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Test Inside temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_inside_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '29.8', + }) +# --- +# name: test_sensors[sensor.test_odometer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_odometer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Odometer', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_odometer', + 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_odometer', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.test_odometer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Test Odometer', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_odometer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10430.189495371', + }) +# --- +# name: test_sensors[sensor.test_odometer-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'distance', + 'friendly_name': 'Test Odometer', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_odometer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10430.189495371', + }) +# --- +# name: test_sensors[sensor.test_outside_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_outside_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outside temperature', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'climate_state_outside_temp', + 'unique_id': 'LRWXF7EK4KC700000-climate_state_outside_temp', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.test_outside_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Test Outside temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_outside_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30', + }) +# --- +# name: test_sensors[sensor.test_outside_temperature-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Test Outside temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_outside_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30', + }) +# --- +# name: test_sensors[sensor.test_passenger_temperature_setting-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_passenger_temperature_setting', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Passenger temperature setting', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'climate_state_passenger_temp_setting', + 'unique_id': 'LRWXF7EK4KC700000-climate_state_passenger_temp_setting', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.test_passenger_temperature_setting-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Test Passenger temperature setting', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_passenger_temperature_setting', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22', + }) +# --- +# name: test_sensors[sensor.test_passenger_temperature_setting-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Test Passenger temperature setting', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_passenger_temperature_setting', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22', + }) +# --- +# name: test_sensors[sensor.test_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drive_state_power', + 'unique_id': 'LRWXF7EK4KC700000-drive_state_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.test_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Test Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-7', + }) +# --- +# name: test_sensors[sensor.test_power-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Test Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-7', + }) +# --- +# name: test_sensors[sensor.test_shift_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'p', + 'd', + 'r', + 'n', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_shift_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Shift state', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drive_state_shift_state', + 'unique_id': 'LRWXF7EK4KC700000-drive_state_shift_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_shift_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Test Shift state', + 'options': list([ + 'p', + 'd', + 'r', + 'n', + ]), + }), + 'context': , + 'entity_id': 'sensor.test_shift_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'p', + }) +# --- +# name: test_sensors[sensor.test_shift_state-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Test Shift state', + 'options': list([ + 'p', + 'd', + 'r', + 'n', + ]), + }), + 'context': , + 'entity_id': 'sensor.test_shift_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'p', + }) +# --- +# name: test_sensors[sensor.test_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Speed', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drive_state_speed', + 'unique_id': 'LRWXF7EK4KC700000-drive_state_speed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.test_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'speed', + 'friendly_name': 'Test Speed', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[sensor.test_speed-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'speed', + 'friendly_name': 'Test Speed', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[sensor.test_state_of_charge_at_arrival-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_state_of_charge_at_arrival', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'State of charge at arrival', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drive_state_active_route_energy_at_arrival', + 'unique_id': 'LRWXF7EK4KC700000-drive_state_active_route_energy_at_arrival', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.test_state_of_charge_at_arrival-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Test State of charge at arrival', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.test_state_of_charge_at_arrival', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_state_of_charge_at_arrival-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Test State of charge at arrival', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.test_state_of_charge_at_arrival', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_time_to_arrival-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_time_to_arrival', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Time to arrival', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drive_state_active_route_minutes_to_arrival', + 'unique_id': 'LRWXF7EK4KC700000-drive_state_active_route_minutes_to_arrival', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_time_to_arrival-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Test Time to arrival', + }), + 'context': , + 'entity_id': 'sensor.test_time_to_arrival', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-01T00:00:06+00:00', + }) +# --- +# name: test_sensors[sensor.test_time_to_arrival-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Test Time to arrival', + }), + 'context': , + 'entity_id': 'sensor.test_time_to_arrival', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.test_time_to_full_charge-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_time_to_full_charge', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Time to full charge', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_minutes_to_full_charge', + 'unique_id': 'LRWXF7EK4KC700000-charge_state_minutes_to_full_charge', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_time_to_full_charge-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Test Time to full charge', + }), + 'context': , + 'entity_id': 'sensor.test_time_to_full_charge', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.test_time_to_full_charge-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Test Time to full charge', + }), + 'context': , + 'entity_id': 'sensor.test_time_to_full_charge', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_front_left-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_tire_pressure_front_left', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tire pressure front left', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_tpms_pressure_fl', + 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_tpms_pressure_fl', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_front_left-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Test Tire pressure front left', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_tire_pressure_front_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.2479739314961', + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_front_left-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Test Tire pressure front left', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_tire_pressure_front_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.2479739314961', + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_front_right-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_tire_pressure_front_right', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tire pressure front right', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_tpms_pressure_fr', + 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_tpms_pressure_fr', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_front_right-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Test Tire pressure front right', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_tire_pressure_front_right', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.6105682912393', + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_front_right-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Test Tire pressure front right', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_tire_pressure_front_right', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.6105682912393', + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_rear_left-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_tire_pressure_rear_left', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tire pressure rear left', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_tpms_pressure_rl', + 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_tpms_pressure_rl', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_rear_left-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Test Tire pressure rear left', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_tire_pressure_rear_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.2479739314961', + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_rear_left-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Test Tire pressure rear left', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_tire_pressure_rear_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.2479739314961', + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_rear_right-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_tire_pressure_rear_right', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tire pressure rear right', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_tpms_pressure_rr', + 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_tpms_pressure_rr', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_rear_right-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Test Tire pressure rear right', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_tire_pressure_rear_right', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.2479739314961', + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_rear_right-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Test Tire pressure rear right', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_tire_pressure_rear_right', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.2479739314961', + }) +# --- +# name: test_sensors[sensor.test_traffic_delay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_traffic_delay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Traffic delay', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drive_state_active_route_traffic_minutes_delay', + 'unique_id': 'LRWXF7EK4KC700000-drive_state_active_route_traffic_minutes_delay', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.test_traffic_delay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Test Traffic delay', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_traffic_delay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[sensor.test_traffic_delay-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Test Traffic delay', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_traffic_delay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[sensor.test_usable_battery_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_usable_battery_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Usable battery level', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_usable_battery_level', + 'unique_id': 'LRWXF7EK4KC700000-charge_state_usable_battery_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.test_usable_battery_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Test Usable battery level', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.test_usable_battery_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '77', + }) +# --- +# name: test_sensors[sensor.test_usable_battery_level-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Test Usable battery level', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.test_usable_battery_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '77', + }) +# --- +# name: test_sensors[sensor.wall_connector_fault_state_code-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.wall_connector_fault_state_code', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Fault state code', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wall_connector_fault_state', + 'unique_id': '123456-abd-123-wall_connector_fault_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.wall_connector_fault_state_code-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector Fault state code', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_fault_state_code', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_sensors[sensor.wall_connector_fault_state_code-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector Fault state code', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_fault_state_code', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_sensors[sensor.wall_connector_fault_state_code_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.wall_connector_fault_state_code_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Fault state code', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wall_connector_fault_state', + 'unique_id': '123456-bcd-234-wall_connector_fault_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.wall_connector_fault_state_code_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector Fault state code', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_fault_state_code_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_sensors[sensor.wall_connector_fault_state_code_2-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector Fault state code', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_fault_state_code_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_sensors[sensor.wall_connector_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wall_connector_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wall_connector_power', + 'unique_id': '123456-abd-123-wall_connector_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.wall_connector_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Wall Connector Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.wall_connector_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.wall_connector_power-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Wall Connector Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.wall_connector_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.wall_connector_power_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wall_connector_power_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wall_connector_power', + 'unique_id': '123456-bcd-234-wall_connector_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.wall_connector_power_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Wall Connector Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.wall_connector_power_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.wall_connector_power_2-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Wall Connector Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.wall_connector_power_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.wall_connector_state_code-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.wall_connector_state_code', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'State code', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wall_connector_state', + 'unique_id': '123456-abd-123-wall_connector_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.wall_connector_state_code-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector State code', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_state_code', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_sensors[sensor.wall_connector_state_code-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector State code', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_state_code', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_sensors[sensor.wall_connector_state_code_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.wall_connector_state_code_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'State code', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wall_connector_state', + 'unique_id': '123456-bcd-234-wall_connector_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.wall_connector_state_code_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector State code', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_state_code_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_sensors[sensor.wall_connector_state_code_2-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector State code', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_state_code_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_sensors[sensor.wall_connector_vehicle-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wall_connector_vehicle', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Vehicle', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vin', + 'unique_id': '123456-abd-123-vin', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.wall_connector_vehicle-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector Vehicle', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_vehicle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.wall_connector_vehicle-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector Vehicle', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_vehicle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.wall_connector_vehicle_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wall_connector_vehicle_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Vehicle', + 'platform': 'tesla_fleet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vin', + 'unique_id': '123456-bcd-234-vin', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.wall_connector_vehicle_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector Vehicle', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_vehicle_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.wall_connector_vehicle_2-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector Vehicle', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_vehicle_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/tesla_fleet/test_config_flow.py b/tests/components/tesla_fleet/test_config_flow.py new file mode 100644 index 00000000000..334d8902fc7 --- /dev/null +++ b/tests/components/tesla_fleet/test_config_flow.py @@ -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" diff --git a/tests/components/tesla_fleet/test_init.py b/tests/components/tesla_fleet/test_init.py new file mode 100644 index 00000000000..4e6352efc6b --- /dev/null +++ b/tests/components/tesla_fleet/test_init.py @@ -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 diff --git a/tests/components/tesla_fleet/test_sensor.py b/tests/components/tesla_fleet/test_sensor.py new file mode 100644 index 00000000000..2133194e2a0 --- /dev/null +++ b/tests/components/tesla_fleet/test_sensor.py @@ -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)