Add Tessie Integration (#104684)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
327016eaeb
commit
64a5271a51
22 changed files with 1518 additions and 0 deletions
|
@ -1303,6 +1303,8 @@ build.json @home-assistant/supervisor
|
|||
/tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core
|
||||
/homeassistant/components/tesla_wall_connector/ @einarhauks
|
||||
/tests/components/tesla_wall_connector/ @einarhauks
|
||||
/homeassistant/components/tessie/ @Bre77
|
||||
/tests/components/tessie/ @Bre77
|
||||
/homeassistant/components/text/ @home-assistant/core
|
||||
/tests/components/text/ @home-assistant/core
|
||||
/homeassistant/components/tfiac/ @fredrike @mellado
|
||||
|
|
60
homeassistant/components/tessie/__init__.py
Normal file
60
homeassistant/components/tessie/__init__.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
"""Tessie integration."""
|
||||
import logging
|
||||
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
from tessie_api import get_state_of_all_vehicles
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TessieDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Tessie config."""
|
||||
api_key = entry.data[CONF_ACCESS_TOKEN]
|
||||
|
||||
try:
|
||||
vehicles = await get_state_of_all_vehicles(
|
||||
session=async_get_clientsession(hass),
|
||||
api_key=api_key,
|
||||
only_active=True,
|
||||
)
|
||||
except ClientResponseError as ex:
|
||||
# Reauth will go here
|
||||
_LOGGER.error("Setup failed, unable to connect to Tessie: %s", ex)
|
||||
return False
|
||||
except ClientError as e:
|
||||
raise ConfigEntryNotReady from e
|
||||
|
||||
coordinators = [
|
||||
TessieDataUpdateCoordinator(
|
||||
hass,
|
||||
api_key=api_key,
|
||||
vin=vehicle["vin"],
|
||||
data=vehicle["last_state"],
|
||||
)
|
||||
for vehicle in vehicles["results"]
|
||||
if vehicle["last_state"] is not None
|
||||
]
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinators
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload Tessie Config."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
56
homeassistant/components/tessie/config_flow.py
Normal file
56
homeassistant/components/tessie/config_flow.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
"""Config Flow for Tessie integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientConnectionError, ClientResponseError
|
||||
from tessie_api import get_state_of_all_vehicles
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
TESSIE_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str})
|
||||
|
||||
|
||||
class TessieConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Config Tessie API connection."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: Mapping[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Get configuration from the user."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input and CONF_ACCESS_TOKEN in user_input:
|
||||
try:
|
||||
await get_state_of_all_vehicles(
|
||||
session=async_get_clientsession(self.hass),
|
||||
api_key=user_input[CONF_ACCESS_TOKEN],
|
||||
only_active=True,
|
||||
)
|
||||
except ClientResponseError as e:
|
||||
if e.status == HTTPStatus.UNAUTHORIZED:
|
||||
errors[CONF_ACCESS_TOKEN] = "invalid_access_token"
|
||||
else:
|
||||
errors["base"] = "unknown"
|
||||
except ClientConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title="Tessie",
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=TESSIE_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
21
homeassistant/components/tessie/const.py
Normal file
21
homeassistant/components/tessie/const.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
"""Constants used by Tessie integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
DOMAIN = "tessie"
|
||||
|
||||
MODELS = {
|
||||
"model3": "Model 3",
|
||||
"modelx": "Model X",
|
||||
"modely": "Model Y",
|
||||
"models": "Model S",
|
||||
}
|
||||
|
||||
|
||||
class TessieStatus(StrEnum):
|
||||
"""Tessie status."""
|
||||
|
||||
ASLEEP = "asleep"
|
||||
ONLINE = "online"
|
||||
OFFLINE = "offline"
|
84
homeassistant/components/tessie/coordinator.py
Normal file
84
homeassistant/components/tessie/coordinator.py
Normal file
|
@ -0,0 +1,84 @@
|
|||
"""Tessie Data Coordinator."""
|
||||
from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from tessie_api import get_state
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import TessieStatus
|
||||
|
||||
# This matches the update interval Tessie performs server side
|
||||
TESSIE_SYNC_INTERVAL = 10
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TessieDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to manage fetching data from the Tessie API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
api_key: str,
|
||||
vin: str,
|
||||
data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize Tessie Data Update Coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="Tessie",
|
||||
update_method=self.async_update_data,
|
||||
update_interval=timedelta(seconds=TESSIE_SYNC_INTERVAL),
|
||||
)
|
||||
self.api_key = api_key
|
||||
self.vin = vin
|
||||
self.session = async_get_clientsession(hass)
|
||||
self.data = self._flattern(data)
|
||||
self.did_first_update = False
|
||||
|
||||
async def async_update_data(self) -> dict[str, Any]:
|
||||
"""Update vehicle data using Tessie API."""
|
||||
try:
|
||||
vehicle = await get_state(
|
||||
session=self.session,
|
||||
api_key=self.api_key,
|
||||
vin=self.vin,
|
||||
use_cache=self.did_first_update,
|
||||
)
|
||||
except ClientResponseError as e:
|
||||
if e.status == HTTPStatus.REQUEST_TIMEOUT:
|
||||
# Vehicle is offline, only update state and dont throw error
|
||||
self.data["state"] = TessieStatus.OFFLINE
|
||||
return self.data
|
||||
# Reauth will go here
|
||||
raise e
|
||||
|
||||
self.did_first_update = True
|
||||
if vehicle["state"] == TessieStatus.ONLINE:
|
||||
# Vehicle is online, all data is fresh
|
||||
return self._flattern(vehicle)
|
||||
|
||||
# Vehicle is asleep, only update state
|
||||
self.data["state"] = vehicle["state"]
|
||||
return self.data
|
||||
|
||||
def _flattern(
|
||||
self, data: dict[str, Any], parent: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Flattern the data structure."""
|
||||
result = {}
|
||||
for key, value in data.items():
|
||||
if parent:
|
||||
key = f"{parent}-{key}"
|
||||
if isinstance(value, dict):
|
||||
result.update(self._flattern(value, key))
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
45
homeassistant/components/tessie/entity.py
Normal file
45
homeassistant/components/tessie/entity.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
"""Tessie parent entity class."""
|
||||
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, MODELS
|
||||
from .coordinator import TessieDataUpdateCoordinator
|
||||
|
||||
|
||||
class TessieEntity(CoordinatorEntity[TessieDataUpdateCoordinator]):
|
||||
"""Parent class for Tessie Entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: TessieDataUpdateCoordinator,
|
||||
key: str,
|
||||
) -> None:
|
||||
"""Initialize common aspects of a Tessie entity."""
|
||||
super().__init__(coordinator)
|
||||
self.vin = coordinator.vin
|
||||
self.key = key
|
||||
|
||||
car_type = coordinator.data["vehicle_config-car_type"]
|
||||
|
||||
self._attr_translation_key = key
|
||||
self._attr_unique_id = f"{self.vin}-{key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self.vin)},
|
||||
manufacturer="Tesla",
|
||||
configuration_url="https://my.tessie.com/",
|
||||
name=coordinator.data["display_name"],
|
||||
model=MODELS.get(car_type, car_type),
|
||||
sw_version=coordinator.data["vehicle_state-car_version"],
|
||||
hw_version=coordinator.data["vehicle_config-driver_assist"],
|
||||
)
|
||||
|
||||
@property
|
||||
def value(self) -> Any:
|
||||
"""Return value from coordinator data."""
|
||||
return self.coordinator.data[self.key]
|
10
homeassistant/components/tessie/manifest.json
Normal file
10
homeassistant/components/tessie/manifest.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"domain": "tessie",
|
||||
"name": "Tessie",
|
||||
"codeowners": ["@Bre77"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/tessie",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tessie"],
|
||||
"requirements": ["tessie-api==0.0.9"]
|
||||
}
|
225
homeassistant/components/tessie/sensor.py
Normal file
225
homeassistant/components/tessie/sensor.py
Normal file
|
@ -0,0 +1,225 @@
|
|||
"""Sensor platform for Tessie integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
UnitOfLength,
|
||||
UnitOfPower,
|
||||
UnitOfPressure,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import DOMAIN, TessieStatus
|
||||
from .coordinator import TessieDataUpdateCoordinator
|
||||
from .entity import TessieEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class TessieSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes Tessie Sensor entity."""
|
||||
|
||||
value_fn: Callable[[StateType], StateType] = lambda x: x
|
||||
|
||||
|
||||
DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = (
|
||||
TessieSensorEntityDescription(
|
||||
key="state",
|
||||
options=[status.value for status in TessieStatus],
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
key="charge_state-usable_battery_level",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
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,
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
key="charge_state-charger_power",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
key="charge_state-charger_voltage",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
key="charge_state-charger_actual_current",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
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,
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
key="charge_state-battery_range",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfLength.MILES,
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
key="drive_state-speed",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR,
|
||||
device_class=SensorDeviceClass.SPEED,
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
key="drive_state-power",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
key="drive_state-shift_state",
|
||||
icon="mdi:car-shift-pattern",
|
||||
options=["p", "d", "r", "n"],
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
value_fn=lambda x: x.lower() if isinstance(x, str) else x,
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
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,
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
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,
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
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,
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
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,
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
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,
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
key="climate_state-inside_temp",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
key="climate_state-outside_temp",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
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,
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
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,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the Tessie sensor platform from a config entry."""
|
||||
coordinators = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
TessieSensorEntity(coordinator, description)
|
||||
for coordinator in coordinators
|
||||
for description in DESCRIPTIONS
|
||||
if description.key in coordinator.data
|
||||
)
|
||||
|
||||
|
||||
class TessieSensorEntity(TessieEntity, SensorEntity):
|
||||
"""Base class for Tessie metric sensors."""
|
||||
|
||||
entity_description: TessieSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: TessieDataUpdateCoordinator,
|
||||
description: TessieSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator, description.key)
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.value)
|
92
homeassistant/components/tessie/strings.json
Normal file
92
homeassistant/components/tessie/strings.json
Normal file
|
@ -0,0 +1,92 @@
|
|||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"access_token": "[%key:common::config_flow::data::access_token%]"
|
||||
},
|
||||
"description": "Enter your access token from [my.tessie.com/settings/api](https://my.tessie.com/settings/api)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"state": {
|
||||
"name": "Status",
|
||||
"state": {
|
||||
"online": "Online",
|
||||
"asleep": "Asleep",
|
||||
"offline": "Offline"
|
||||
}
|
||||
},
|
||||
"charge_state-usable_battery_level": {
|
||||
"name": "Battery Level"
|
||||
},
|
||||
"charge_state-charge_energy_added": {
|
||||
"name": "Charge Energy Added"
|
||||
},
|
||||
"charge_state-charger_power": {
|
||||
"name": "Charger Power"
|
||||
},
|
||||
"charge_state-charger_voltage": {
|
||||
"name": "Charger Voltage"
|
||||
},
|
||||
"charge_state-charger_actual_current": {
|
||||
"name": "Charger Current"
|
||||
},
|
||||
"charge_state-charge_rate": {
|
||||
"name": "Charge Rate"
|
||||
},
|
||||
"charge_state-battery_range": {
|
||||
"name": "Battery Range"
|
||||
},
|
||||
"drive_state-speed": {
|
||||
"name": "Speed"
|
||||
},
|
||||
"drive_state-power": {
|
||||
"name": "Power"
|
||||
},
|
||||
"drive_state-shift_state": {
|
||||
"name": "Shift State",
|
||||
"state": {
|
||||
"p": "Park",
|
||||
"d": "Drive",
|
||||
"r": "Reverse",
|
||||
"n": "Neutral"
|
||||
}
|
||||
},
|
||||
"vehicle_state-odometer": {
|
||||
"name": "Odometer"
|
||||
},
|
||||
"vehicle_state-tpms_pressure_fl": {
|
||||
"name": "Tyre Pressure Front Left"
|
||||
},
|
||||
"vehicle_state-tpms_pressure_fr": {
|
||||
"name": "Tyre Pressure Front Right"
|
||||
},
|
||||
"vehicle_state-tpms_pressure_rl": {
|
||||
"name": "Tyre Pressure Rear Left"
|
||||
},
|
||||
"vehicle_state-tpms_pressure_rr": {
|
||||
"name": "Tyre Pressure Rear Right"
|
||||
},
|
||||
"climate_state-inside_temp": {
|
||||
"name": "Inside Temperature"
|
||||
},
|
||||
"climate_state-outside_temp": {
|
||||
"name": "Outside Temperature"
|
||||
},
|
||||
"climate_state-driver_temp_setting": {
|
||||
"name": "Driver Temperature Setting"
|
||||
},
|
||||
"climate_state-passenger_temp_setting": {
|
||||
"name": "Passenger Temperature Setting"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -490,6 +490,7 @@ FLOWS = {
|
|||
"tautulli",
|
||||
"tellduslive",
|
||||
"tesla_wall_connector",
|
||||
"tessie",
|
||||
"thermobeacon",
|
||||
"thermopro",
|
||||
"thread",
|
||||
|
|
|
@ -5802,6 +5802,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"tessie": {
|
||||
"name": "Tessie",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"tfiac": {
|
||||
"name": "Tfiac",
|
||||
"integration_type": "hub",
|
||||
|
|
|
@ -2603,6 +2603,9 @@ tesla-powerwall==0.3.19
|
|||
# homeassistant.components.tesla_wall_connector
|
||||
tesla-wall-connector==1.0.2
|
||||
|
||||
# homeassistant.components.tessie
|
||||
tessie-api==0.0.9
|
||||
|
||||
# homeassistant.components.tensorflow
|
||||
# tf-models-official==2.5.0
|
||||
|
||||
|
|
|
@ -1943,6 +1943,9 @@ tesla-powerwall==0.3.19
|
|||
# homeassistant.components.tesla_wall_connector
|
||||
tesla-wall-connector==1.0.2
|
||||
|
||||
# homeassistant.components.tessie
|
||||
tessie-api==0.0.9
|
||||
|
||||
# homeassistant.components.thermobeacon
|
||||
thermobeacon-ble==0.6.0
|
||||
|
||||
|
|
1
tests/components/tessie/__init__.py
Normal file
1
tests/components/tessie/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Tests for the Tessie integration."""
|
55
tests/components/tessie/common.py
Normal file
55
tests/components/tessie/common.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
"""Tessie common helpers for tests."""
|
||||
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import patch
|
||||
|
||||
from aiohttp import ClientConnectionError, ClientResponseError
|
||||
from aiohttp.client import RequestInfo
|
||||
|
||||
from homeassistant.components.tessie.const import DOMAIN
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||
|
||||
TEST_STATE_OF_ALL_VEHICLES = load_json_object_fixture("vehicles.json", DOMAIN)
|
||||
TEST_VEHICLE_STATE_ONLINE = load_json_object_fixture("online.json", DOMAIN)
|
||||
TEST_VEHICLE_STATE_ASLEEP = load_json_object_fixture("asleep.json", DOMAIN)
|
||||
|
||||
TEST_CONFIG = {CONF_ACCESS_TOKEN: "1234567890"}
|
||||
TESSIE_URL = "https://api.tessie.com/"
|
||||
|
||||
TEST_REQUEST_INFO = RequestInfo(
|
||||
url=TESSIE_URL, method="GET", headers={}, real_url=TESSIE_URL
|
||||
)
|
||||
|
||||
ERROR_AUTH = ClientResponseError(
|
||||
request_info=TEST_REQUEST_INFO, history=None, status=HTTPStatus.UNAUTHORIZED
|
||||
)
|
||||
ERROR_TIMEOUT = ClientResponseError(
|
||||
request_info=TEST_REQUEST_INFO, history=None, status=HTTPStatus.REQUEST_TIMEOUT
|
||||
)
|
||||
ERROR_UNKNOWN = ClientResponseError(
|
||||
request_info=TEST_REQUEST_INFO, history=None, status=HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
ERROR_CONNECTION = ClientConnectionError()
|
||||
|
||||
|
||||
async def setup_platform(hass: HomeAssistant, side_effect=None):
|
||||
"""Set up the Tessie platform."""
|
||||
|
||||
mock_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=TEST_CONFIG,
|
||||
)
|
||||
mock_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tessie.get_state_of_all_vehicles",
|
||||
return_value=TEST_STATE_OF_ALL_VEHICLES,
|
||||
side_effect=side_effect,
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return mock_entry
|
1
tests/components/tessie/fixtures/asleep.json
Normal file
1
tests/components/tessie/fixtures/asleep.json
Normal file
|
@ -0,0 +1 @@
|
|||
{ "state": "asleep" }
|
276
tests/components/tessie/fixtures/online.json
Normal file
276
tests/components/tessie/fixtures/online.json
Normal file
|
@ -0,0 +1,276 @@
|
|||
{
|
||||
"user_id": 234567890,
|
||||
"vehicle_id": 345678901,
|
||||
"vin": "VINVINVIN",
|
||||
"color": null,
|
||||
"access_type": "OWNER",
|
||||
"granular_access": {
|
||||
"hide_private": false
|
||||
},
|
||||
"tokens": ["beef", "c0ffee"],
|
||||
"state": "online",
|
||||
"in_service": false,
|
||||
"id_s": "123456789",
|
||||
"calendar_enabled": true,
|
||||
"api_version": 67,
|
||||
"backseat_token": null,
|
||||
"backseat_token_updated_at": null,
|
||||
"ble_autopair_enrolled": false,
|
||||
"charge_state": {
|
||||
"battery_heater_on": false,
|
||||
"battery_level": 75,
|
||||
"battery_range": 263.68,
|
||||
"charge_amps": 32,
|
||||
"charge_current_request": 32,
|
||||
"charge_current_request_max": 32,
|
||||
"charge_enable_request": true,
|
||||
"charge_energy_added": 18.47,
|
||||
"charge_limit_soc": 80,
|
||||
"charge_limit_soc_max": 100,
|
||||
"charge_limit_soc_min": 50,
|
||||
"charge_limit_soc_std": 80,
|
||||
"charge_miles_added_ideal": 84,
|
||||
"charge_miles_added_rated": 84,
|
||||
"charge_port_cold_weather_mode": false,
|
||||
"charge_port_color": "<invalid>",
|
||||
"charge_port_door_open": true,
|
||||
"charge_port_latch": "Engaged",
|
||||
"charge_rate": 30.6,
|
||||
"charger_actual_current": 32,
|
||||
"charger_phases": 1,
|
||||
"charger_pilot_current": 32,
|
||||
"charger_power": 7,
|
||||
"charger_voltage": 224,
|
||||
"charging_state": "Charging",
|
||||
"conn_charge_cable": "IEC",
|
||||
"est_battery_range": 324.73,
|
||||
"fast_charger_brand": "",
|
||||
"fast_charger_present": false,
|
||||
"fast_charger_type": "ACSingleWireCAN",
|
||||
"ideal_battery_range": 263.68,
|
||||
"max_range_charge_counter": 0,
|
||||
"minutes_to_full_charge": 30,
|
||||
"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": "StartAt",
|
||||
"scheduled_charging_pending": false,
|
||||
"scheduled_charging_start_time": 1701216000,
|
||||
"scheduled_charging_start_time_app": 600,
|
||||
"scheduled_charging_start_time_minutes": 600,
|
||||
"scheduled_departure_time": 1694899800,
|
||||
"scheduled_departure_time_minutes": 450,
|
||||
"supercharger_session_trip_planner": false,
|
||||
"time_to_full_charge": 0.5,
|
||||
"timestamp": 1701139037461,
|
||||
"trip_charging": false,
|
||||
"usable_battery_level": 75,
|
||||
"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": true,
|
||||
"battery_heater": false,
|
||||
"battery_heater_no_power": null,
|
||||
"cabin_overheat_protection": "On",
|
||||
"cabin_overheat_protection_actively_cooling": false,
|
||||
"climate_keeper_mode": "off",
|
||||
"cop_activation_temperature": "High",
|
||||
"defrost_mode": 0,
|
||||
"driver_temp_setting": 22.5,
|
||||
"fan_status": 0,
|
||||
"hvac_auto_request": "On",
|
||||
"inside_temp": 30.4,
|
||||
"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": 234,
|
||||
"max_avail_temp": 28,
|
||||
"min_avail_temp": 15,
|
||||
"outside_temp": 30.5,
|
||||
"passenger_temp_setting": 22.5,
|
||||
"remote_heater_control_enabled": false,
|
||||
"right_temp_direction": 234,
|
||||
"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": 1701139037461,
|
||||
"wiper_blade_heater": false
|
||||
},
|
||||
"drive_state": {
|
||||
"active_route_latitude": 30.2226265,
|
||||
"active_route_longitude": -97.6236871,
|
||||
"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": 1701139037461
|
||||
},
|
||||
"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": 1701139037461
|
||||
},
|
||||
"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": false,
|
||||
"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": 1701139037461,
|
||||
"trim_badging": "74d",
|
||||
"use_range_badging": true,
|
||||
"utc_offset": 36000,
|
||||
"webcam_selfie_supported": true,
|
||||
"webcam_supported": true,
|
||||
"wheel_type": "Pinwheel18CapKit"
|
||||
},
|
||||
"vehicle_state": {
|
||||
"api_version": 67,
|
||||
"autopark_state_v2": "unavailable",
|
||||
"calendar_supported": true,
|
||||
"car_version": "2023.38.6 c1f85ddb415f",
|
||||
"center_display_state": 0,
|
||||
"dashcam_clip_save_available": true,
|
||||
"dashcam_state": "Recording",
|
||||
"df": 0,
|
||||
"dr": 0,
|
||||
"fd_window": 0,
|
||||
"feature_bitmask": "fbdffbff,7f",
|
||||
"fp_window": 0,
|
||||
"ft": 0,
|
||||
"is_user_present": false,
|
||||
"locked": true,
|
||||
"media_info": {
|
||||
"audio_volume": 2.3333,
|
||||
"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": false
|
||||
},
|
||||
"notifications_supported": true,
|
||||
"odometer": 5454.495383,
|
||||
"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": 0,
|
||||
"expected_duration_sec": 2700,
|
||||
"install_perc": 1,
|
||||
"status": "",
|
||||
"version": " "
|
||||
},
|
||||
"speed_limit_mode": {
|
||||
"active": false,
|
||||
"current_limit_mph": 74.564543,
|
||||
"max_limit_mph": 120,
|
||||
"min_limit_mph": 50,
|
||||
"pin_code_set": true
|
||||
},
|
||||
"timestamp": 1701139037461,
|
||||
"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": 1701062077,
|
||||
"tpms_last_seen_pressure_time_fr": 1701062047,
|
||||
"tpms_last_seen_pressure_time_rl": 1701062077,
|
||||
"tpms_last_seen_pressure_time_rr": 1701062047,
|
||||
"tpms_pressure_fl": 2.975,
|
||||
"tpms_pressure_fr": 2.975,
|
||||
"tpms_pressure_rl": 2.95,
|
||||
"tpms_pressure_rr": 2.95,
|
||||
"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
|
||||
},
|
||||
"display_name": "Test"
|
||||
}
|
292
tests/components/tessie/fixtures/vehicles.json
Normal file
292
tests/components/tessie/fixtures/vehicles.json
Normal file
|
@ -0,0 +1,292 @@
|
|||
{
|
||||
"results": [
|
||||
{
|
||||
"vin": "VINVINVIN",
|
||||
"is_active": true,
|
||||
"is_archived_manually": false,
|
||||
"last_charge_created_at": null,
|
||||
"last_charge_updated_at": null,
|
||||
"last_drive_created_at": null,
|
||||
"last_drive_updated_at": null,
|
||||
"last_idle_created_at": null,
|
||||
"last_idle_updated_at": null,
|
||||
"last_state": {
|
||||
"id": 123456789,
|
||||
"user_id": 234567890,
|
||||
"vehicle_id": 345678901,
|
||||
"vin": "VINVINVIN",
|
||||
"color": null,
|
||||
"access_type": "OWNER",
|
||||
"granular_access": {
|
||||
"hide_private": false
|
||||
},
|
||||
"tokens": ["beef", "c0ffee"],
|
||||
"state": "online",
|
||||
"in_service": false,
|
||||
"id_s": "123456789",
|
||||
"calendar_enabled": true,
|
||||
"api_version": 67,
|
||||
"backseat_token": null,
|
||||
"backseat_token_updated_at": null,
|
||||
"ble_autopair_enrolled": false,
|
||||
"charge_state": {
|
||||
"battery_heater_on": false,
|
||||
"battery_level": 75,
|
||||
"battery_range": 263.68,
|
||||
"charge_amps": 32,
|
||||
"charge_current_request": 32,
|
||||
"charge_current_request_max": 32,
|
||||
"charge_enable_request": true,
|
||||
"charge_energy_added": 18.47,
|
||||
"charge_limit_soc": 80,
|
||||
"charge_limit_soc_max": 100,
|
||||
"charge_limit_soc_min": 50,
|
||||
"charge_limit_soc_std": 80,
|
||||
"charge_miles_added_ideal": 84,
|
||||
"charge_miles_added_rated": 84,
|
||||
"charge_port_cold_weather_mode": false,
|
||||
"charge_port_color": "<invalid>",
|
||||
"charge_port_door_open": true,
|
||||
"charge_port_latch": "Engaged",
|
||||
"charge_rate": 30.6,
|
||||
"charger_actual_current": 32,
|
||||
"charger_phases": 1,
|
||||
"charger_pilot_current": 32,
|
||||
"charger_power": 7,
|
||||
"charger_voltage": 224,
|
||||
"charging_state": "Charging",
|
||||
"conn_charge_cable": "IEC",
|
||||
"est_battery_range": 324.73,
|
||||
"fast_charger_brand": "",
|
||||
"fast_charger_present": false,
|
||||
"fast_charger_type": "ACSingleWireCAN",
|
||||
"ideal_battery_range": 263.68,
|
||||
"max_range_charge_counter": 0,
|
||||
"minutes_to_full_charge": 30,
|
||||
"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": "StartAt",
|
||||
"scheduled_charging_pending": false,
|
||||
"scheduled_charging_start_time": 1701216000,
|
||||
"scheduled_charging_start_time_app": 600,
|
||||
"scheduled_charging_start_time_minutes": 600,
|
||||
"scheduled_departure_time": 1694899800,
|
||||
"scheduled_departure_time_minutes": 450,
|
||||
"supercharger_session_trip_planner": false,
|
||||
"time_to_full_charge": 0.5,
|
||||
"timestamp": 1701139037461,
|
||||
"trip_charging": false,
|
||||
"usable_battery_level": 75,
|
||||
"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": true,
|
||||
"battery_heater": false,
|
||||
"battery_heater_no_power": null,
|
||||
"cabin_overheat_protection": "On",
|
||||
"cabin_overheat_protection_actively_cooling": false,
|
||||
"climate_keeper_mode": "off",
|
||||
"cop_activation_temperature": "High",
|
||||
"defrost_mode": 0,
|
||||
"driver_temp_setting": 22.5,
|
||||
"fan_status": 0,
|
||||
"hvac_auto_request": "On",
|
||||
"inside_temp": 30.4,
|
||||
"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": 234,
|
||||
"max_avail_temp": 28,
|
||||
"min_avail_temp": 15,
|
||||
"outside_temp": 30.5,
|
||||
"passenger_temp_setting": 22.5,
|
||||
"remote_heater_control_enabled": false,
|
||||
"right_temp_direction": 234,
|
||||
"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": 1701139037461,
|
||||
"wiper_blade_heater": false
|
||||
},
|
||||
"drive_state": {
|
||||
"active_route_latitude": 30.2226265,
|
||||
"active_route_longitude": -97.6236871,
|
||||
"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": 1701139037461
|
||||
},
|
||||
"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": 1701139037461
|
||||
},
|
||||
"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": false,
|
||||
"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": 1701139037461,
|
||||
"trim_badging": "74d",
|
||||
"use_range_badging": true,
|
||||
"utc_offset": 36000,
|
||||
"webcam_selfie_supported": true,
|
||||
"webcam_supported": true,
|
||||
"wheel_type": "Pinwheel18CapKit"
|
||||
},
|
||||
"vehicle_state": {
|
||||
"api_version": 67,
|
||||
"autopark_state_v2": "unavailable",
|
||||
"calendar_supported": true,
|
||||
"car_version": "2023.38.6 c1f85ddb415f",
|
||||
"center_display_state": 0,
|
||||
"dashcam_clip_save_available": true,
|
||||
"dashcam_state": "Recording",
|
||||
"df": 0,
|
||||
"dr": 0,
|
||||
"fd_window": 0,
|
||||
"feature_bitmask": "fbdffbff,7f",
|
||||
"fp_window": 0,
|
||||
"ft": 0,
|
||||
"is_user_present": false,
|
||||
"locked": true,
|
||||
"media_info": {
|
||||
"audio_volume": 2.3333,
|
||||
"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": false
|
||||
},
|
||||
"notifications_supported": true,
|
||||
"odometer": 5454.495383,
|
||||
"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": 0,
|
||||
"expected_duration_sec": 2700,
|
||||
"install_perc": 1,
|
||||
"status": "",
|
||||
"version": " "
|
||||
},
|
||||
"speed_limit_mode": {
|
||||
"active": false,
|
||||
"current_limit_mph": 74.564543,
|
||||
"max_limit_mph": 120,
|
||||
"min_limit_mph": 50,
|
||||
"pin_code_set": true
|
||||
},
|
||||
"timestamp": 1701139037461,
|
||||
"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": 1701062077,
|
||||
"tpms_last_seen_pressure_time_fr": 1701062047,
|
||||
"tpms_last_seen_pressure_time_rl": 1701062077,
|
||||
"tpms_last_seen_pressure_time_rr": 1701062047,
|
||||
"tpms_pressure_fl": 2.975,
|
||||
"tpms_pressure_fr": 2.975,
|
||||
"tpms_pressure_rl": 2.95,
|
||||
"tpms_pressure_rr": 2.95,
|
||||
"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
|
||||
},
|
||||
"display_name": "Test"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
139
tests/components/tessie/test_config_flow.py
Normal file
139
tests/components/tessie/test_config_flow.py
Normal file
|
@ -0,0 +1,139 @@
|
|||
"""Test the Tessie config flow."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.tessie.const import DOMAIN
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from .common import (
|
||||
ERROR_AUTH,
|
||||
ERROR_CONNECTION,
|
||||
ERROR_UNKNOWN,
|
||||
TEST_CONFIG,
|
||||
TEST_STATE_OF_ALL_VEHICLES,
|
||||
)
|
||||
|
||||
|
||||
async def test_form(hass: HomeAssistant) -> None:
|
||||
"""Test we get the form."""
|
||||
|
||||
result1 = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result1["type"] == FlowResultType.FORM
|
||||
assert not result1["errors"]
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tessie.config_flow.get_state_of_all_vehicles",
|
||||
return_value=TEST_STATE_OF_ALL_VEHICLES,
|
||||
) as mock_get_state_of_all_vehicles, patch(
|
||||
"homeassistant.components.tessie.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result1["flow_id"],
|
||||
TEST_CONFIG,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
assert len(mock_get_state_of_all_vehicles.mock_calls) == 1
|
||||
|
||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == "Tessie"
|
||||
assert result2["data"] == TEST_CONFIG
|
||||
|
||||
|
||||
async def test_form_invalid_access_token(hass: HomeAssistant) -> None:
|
||||
"""Test invalid auth is handled."""
|
||||
|
||||
result1 = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tessie.config_flow.get_state_of_all_vehicles",
|
||||
side_effect=ERROR_AUTH,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result1["flow_id"],
|
||||
TEST_CONFIG,
|
||||
)
|
||||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["errors"] == {CONF_ACCESS_TOKEN: "invalid_access_token"}
|
||||
|
||||
# Complete the flow
|
||||
with patch(
|
||||
"homeassistant.components.tessie.config_flow.get_state_of_all_vehicles",
|
||||
return_value=TEST_STATE_OF_ALL_VEHICLES,
|
||||
):
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
TEST_CONFIG,
|
||||
)
|
||||
assert result3["type"] == FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_form_invalid_response(hass: HomeAssistant) -> None:
|
||||
"""Test invalid auth is handled."""
|
||||
|
||||
result1 = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tessie.config_flow.get_state_of_all_vehicles",
|
||||
side_effect=ERROR_UNKNOWN,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result1["flow_id"],
|
||||
TEST_CONFIG,
|
||||
)
|
||||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "unknown"}
|
||||
|
||||
# Complete the flow
|
||||
with patch(
|
||||
"homeassistant.components.tessie.config_flow.get_state_of_all_vehicles",
|
||||
return_value=TEST_STATE_OF_ALL_VEHICLES,
|
||||
):
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
TEST_CONFIG,
|
||||
)
|
||||
assert result3["type"] == FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_form_network_issue(hass: HomeAssistant) -> None:
|
||||
"""Test network issues are handled."""
|
||||
|
||||
result1 = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tessie.config_flow.get_state_of_all_vehicles",
|
||||
side_effect=ERROR_CONNECTION,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result1["flow_id"],
|
||||
TEST_CONFIG,
|
||||
)
|
||||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
# Complete the flow
|
||||
with patch(
|
||||
"homeassistant.components.tessie.config_flow.get_state_of_all_vehicles",
|
||||
return_value=TEST_STATE_OF_ALL_VEHICLES,
|
||||
):
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
TEST_CONFIG,
|
||||
)
|
||||
assert result3["type"] == FlowResultType.CREATE_ENTRY
|
92
tests/components/tessie/test_coordinator.py
Normal file
92
tests/components/tessie/test_coordinator.py
Normal file
|
@ -0,0 +1,92 @@
|
|||
"""Test the Tessie sensor platform."""
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.tessie.coordinator import TESSIE_SYNC_INTERVAL
|
||||
from homeassistant.components.tessie.sensor import TessieStatus
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .common import (
|
||||
ERROR_CONNECTION,
|
||||
ERROR_TIMEOUT,
|
||||
ERROR_UNKNOWN,
|
||||
TEST_VEHICLE_STATE_ASLEEP,
|
||||
TEST_VEHICLE_STATE_ONLINE,
|
||||
setup_platform,
|
||||
)
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
WAIT = timedelta(seconds=TESSIE_SYNC_INTERVAL)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_get_state():
|
||||
"""Mock get_state function."""
|
||||
with patch(
|
||||
"homeassistant.components.tessie.coordinator.get_state",
|
||||
) as mock_get_state:
|
||||
yield mock_get_state
|
||||
|
||||
|
||||
async def test_coordinator_online(hass: HomeAssistant, mock_get_state) -> None:
|
||||
"""Tests that the coordinator handles online vehciles."""
|
||||
|
||||
mock_get_state.return_value = TEST_VEHICLE_STATE_ONLINE
|
||||
await setup_platform(hass)
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + WAIT)
|
||||
await hass.async_block_till_done()
|
||||
mock_get_state.assert_called_once()
|
||||
assert hass.states.get("sensor.test_status").state == TessieStatus.ONLINE
|
||||
|
||||
|
||||
async def test_coordinator_asleep(hass: HomeAssistant, mock_get_state) -> None:
|
||||
"""Tests that the coordinator handles asleep vehicles."""
|
||||
|
||||
mock_get_state.return_value = TEST_VEHICLE_STATE_ASLEEP
|
||||
await setup_platform(hass)
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + WAIT)
|
||||
await hass.async_block_till_done()
|
||||
mock_get_state.assert_called_once()
|
||||
assert hass.states.get("sensor.test_status").state == TessieStatus.ASLEEP
|
||||
|
||||
|
||||
async def test_coordinator_clienterror(hass: HomeAssistant, mock_get_state) -> None:
|
||||
"""Tests that the coordinator handles client errors."""
|
||||
|
||||
mock_get_state.side_effect = ERROR_UNKNOWN
|
||||
await setup_platform(hass)
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + WAIT)
|
||||
await hass.async_block_till_done()
|
||||
mock_get_state.assert_called_once()
|
||||
assert hass.states.get("sensor.test_status").state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_coordinator_timeout(hass: HomeAssistant, mock_get_state) -> None:
|
||||
"""Tests that the coordinator handles timeout errors."""
|
||||
|
||||
mock_get_state.side_effect = ERROR_TIMEOUT
|
||||
await setup_platform(hass)
|
||||
|
||||
async_fire_time_changed(hass, utcnow() + WAIT)
|
||||
await hass.async_block_till_done()
|
||||
mock_get_state.assert_called_once()
|
||||
assert hass.states.get("sensor.test_status").state == TessieStatus.OFFLINE
|
||||
|
||||
|
||||
async def test_coordinator_connection(hass: HomeAssistant, mock_get_state) -> None:
|
||||
"""Tests that the coordinator handles connection errors."""
|
||||
|
||||
mock_get_state.side_effect = ERROR_CONNECTION
|
||||
await setup_platform(hass)
|
||||
async_fire_time_changed(hass, utcnow() + WAIT)
|
||||
await hass.async_block_till_done()
|
||||
mock_get_state.assert_called_once()
|
||||
assert hass.states.get("sensor.test_status").state == STATE_UNAVAILABLE
|
30
tests/components/tessie/test_init.py
Normal file
30
tests/components/tessie/test_init.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
"""Test the Tessie init."""
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .common import ERROR_CONNECTION, ERROR_UNKNOWN, setup_platform
|
||||
|
||||
|
||||
async def test_load_unload(hass: HomeAssistant) -> None:
|
||||
"""Test load and unload."""
|
||||
|
||||
entry = await setup_platform(hass)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_unknown_failure(hass: HomeAssistant) -> None:
|
||||
"""Test init with an authentication failure."""
|
||||
|
||||
entry = await setup_platform(hass, side_effect=ERROR_UNKNOWN)
|
||||
assert entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
|
||||
async def test_connection_failure(hass: HomeAssistant) -> None:
|
||||
"""Test init with a network connection failure."""
|
||||
|
||||
entry = await setup_platform(hass, side_effect=ERROR_CONNECTION)
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
24
tests/components/tessie/test_sensor.py
Normal file
24
tests/components/tessie/test_sensor.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
"""Test the Tessie sensor platform."""
|
||||
from homeassistant.components.tessie.sensor import DESCRIPTIONS
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .common import TEST_VEHICLE_STATE_ONLINE, setup_platform
|
||||
|
||||
|
||||
async def test_sensors(hass: HomeAssistant) -> None:
|
||||
"""Tests that the sensors are correct."""
|
||||
|
||||
assert len(hass.states.async_all("sensor")) == 0
|
||||
|
||||
await setup_platform(hass)
|
||||
|
||||
assert len(hass.states.async_all("sensor")) == len(DESCRIPTIONS)
|
||||
|
||||
assert hass.states.get("sensor.test_battery_level").state == str(
|
||||
TEST_VEHICLE_STATE_ONLINE["charge_state"]["battery_level"]
|
||||
)
|
||||
assert hass.states.get("sensor.test_charge_energy_added").state == str(
|
||||
TEST_VEHICLE_STATE_ONLINE["charge_state"]["charge_energy_added"]
|
||||
)
|
||||
assert hass.states.get("sensor.test_shift_state").state == STATE_UNKNOWN
|
Loading…
Add table
Reference in a new issue