mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Add Tessie Integration (#104684)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
327016eaeb
commit
64a5271a51
@ -1303,6 +1303,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core
|
/tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core
|
||||||
/homeassistant/components/tesla_wall_connector/ @einarhauks
|
/homeassistant/components/tesla_wall_connector/ @einarhauks
|
||||||
/tests/components/tesla_wall_connector/ @einarhauks
|
/tests/components/tesla_wall_connector/ @einarhauks
|
||||||
|
/homeassistant/components/tessie/ @Bre77
|
||||||
|
/tests/components/tessie/ @Bre77
|
||||||
/homeassistant/components/text/ @home-assistant/core
|
/homeassistant/components/text/ @home-assistant/core
|
||||||
/tests/components/text/ @home-assistant/core
|
/tests/components/text/ @home-assistant/core
|
||||||
/homeassistant/components/tfiac/ @fredrike @mellado
|
/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",
|
"tautulli",
|
||||||
"tellduslive",
|
"tellduslive",
|
||||||
"tesla_wall_connector",
|
"tesla_wall_connector",
|
||||||
|
"tessie",
|
||||||
"thermobeacon",
|
"thermobeacon",
|
||||||
"thermopro",
|
"thermopro",
|
||||||
"thread",
|
"thread",
|
||||||
|
@ -5802,6 +5802,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"tessie": {
|
||||||
|
"name": "Tessie",
|
||||||
|
"integration_type": "hub",
|
||||||
|
"config_flow": true,
|
||||||
|
"iot_class": "cloud_polling"
|
||||||
|
},
|
||||||
"tfiac": {
|
"tfiac": {
|
||||||
"name": "Tfiac",
|
"name": "Tfiac",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
|
@ -2603,6 +2603,9 @@ tesla-powerwall==0.3.19
|
|||||||
# homeassistant.components.tesla_wall_connector
|
# homeassistant.components.tesla_wall_connector
|
||||||
tesla-wall-connector==1.0.2
|
tesla-wall-connector==1.0.2
|
||||||
|
|
||||||
|
# homeassistant.components.tessie
|
||||||
|
tessie-api==0.0.9
|
||||||
|
|
||||||
# homeassistant.components.tensorflow
|
# homeassistant.components.tensorflow
|
||||||
# tf-models-official==2.5.0
|
# tf-models-official==2.5.0
|
||||||
|
|
||||||
|
@ -1943,6 +1943,9 @@ tesla-powerwall==0.3.19
|
|||||||
# homeassistant.components.tesla_wall_connector
|
# homeassistant.components.tesla_wall_connector
|
||||||
tesla-wall-connector==1.0.2
|
tesla-wall-connector==1.0.2
|
||||||
|
|
||||||
|
# homeassistant.components.tessie
|
||||||
|
tessie-api==0.0.9
|
||||||
|
|
||||||
# homeassistant.components.thermobeacon
|
# homeassistant.components.thermobeacon
|
||||||
thermobeacon-ble==0.6.0
|
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…
x
Reference in New Issue
Block a user