Add battery health sensors to Tessie (#162908)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
Brett Adams
2026-02-17 00:57:05 +10:00
committed by GitHub
parent 46a1dda8d8
commit 8d228b6e6a
15 changed files with 743 additions and 8 deletions

View File

@@ -13,17 +13,22 @@ from tesla_fleet_api.exceptions import (
TeslaFleetError,
)
from tesla_fleet_api.tessie import Tessie
from tessie_api import get_state_of_all_vehicles
from tessie_api import get_battery, 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 ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryError,
ConfigEntryNotReady,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from .const import DOMAIN, MODELS
from .coordinator import (
TessieBatteryHealthCoordinator,
TessieEnergyHistoryCoordinator,
TessieEnergySiteInfoCoordinator,
TessieEnergySiteLiveCoordinator,
@@ -65,8 +70,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo
except ClientResponseError as e:
if e.status == HTTPStatus.UNAUTHORIZED:
raise ConfigEntryAuthFailed from e
_LOGGER.error("Setup failed, unable to connect to Tessie: %s", e)
return False
raise ConfigEntryError("Setup failed, unable to connect to Tessie") from e
except ClientError as e:
raise ConfigEntryNotReady from e
try:
batteries = await asyncio.gather(
*(
get_battery(
session=session,
api_key=api_key,
vin=vehicle["vin"],
)
for vehicle in state_of_all_vehicles["results"]
if vehicle["last_state"] is not None
)
)
except ClientResponseError as e:
if e.status == HTTPStatus.UNAUTHORIZED:
raise ConfigEntryAuthFailed from e
raise ConfigEntryError("Setup failed, unable to get battery data") from e
except ClientError as e:
raise ConfigEntryNotReady from e
@@ -80,6 +103,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo
vin=vehicle["vin"],
data=vehicle["last_state"],
),
battery_coordinator=TessieBatteryHealthCoordinator(
hass,
entry,
api_key=api_key,
vin=vehicle["vin"],
data=battery,
),
device=DeviceInfo(
identifiers={(DOMAIN, vehicle["vin"])},
manufacturer="Tesla",
@@ -96,8 +126,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo
serial_number=vehicle["vin"],
),
)
for vehicle in state_of_all_vehicles["results"]
if vehicle["last_state"] is not None
for vehicle, battery in zip(
(
v
for v in state_of_all_vehicles["results"]
if v["last_state"] is not None
),
batteries,
strict=True,
)
]
# Energy Sites

View File

@@ -11,7 +11,7 @@ from aiohttp import ClientResponseError
from tesla_fleet_api.const import TeslaEnergyPeriod
from tesla_fleet_api.exceptions import InvalidToken, MissingToken, TeslaFleetError
from tesla_fleet_api.tessie import EnergySite
from tessie_api import get_state, get_status
from tessie_api import get_battery, get_state, get_status
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
@@ -99,6 +99,48 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
return flatten(vehicle)
class TessieBatteryHealthCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Class to manage fetching battery health data from the Tessie API."""
config_entry: TessieConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: TessieConfigEntry,
api_key: str,
vin: str,
data: dict[str, Any],
) -> None:
"""Initialize Tessie Battery Health coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name="Tessie Battery Health",
update_interval=timedelta(seconds=TESSIE_SYNC_INTERVAL),
)
self.api_key = api_key
self.vin = vin
self.session = async_get_clientsession(hass)
self.data = data
async def _async_update_data(self) -> dict[str, Any]:
"""Update battery health data using Tessie API."""
try:
data = await get_battery(
session=self.session,
api_key=self.api_key,
vin=self.vin,
)
except ClientResponseError as e:
if e.status == HTTPStatus.UNAUTHORIZED:
raise ConfigEntryAuthFailed from e
raise UpdateFailed from e
return data
class TessieEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Class to manage fetching energy site live status from the Tessie API."""

View File

@@ -35,7 +35,7 @@ async def async_get_config_entry_diagnostics(
vehicles = [
{
"data": async_redact_data(x.data_coordinator.data, VEHICLE_REDACT),
# Battery diag will go here when implemented
"battery": x.battery_coordinator.data,
}
for x in entry.runtime_data.vehicles
]

View File

@@ -12,6 +12,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, TRANSLATED_ERRORS
from .coordinator import (
TessieBatteryHealthCoordinator,
TessieEnergyHistoryCoordinator,
TessieEnergySiteInfoCoordinator,
TessieEnergySiteLiveCoordinator,
@@ -23,6 +24,7 @@ from .models import TessieEnergyData, TessieVehicleData
class TessieBaseEntity(
CoordinatorEntity[
TessieStateUpdateCoordinator
| TessieBatteryHealthCoordinator
| TessieEnergySiteInfoCoordinator
| TessieEnergySiteLiveCoordinator
| TessieEnergyHistoryCoordinator
@@ -35,6 +37,7 @@ class TessieBaseEntity(
def __init__(
self,
coordinator: TessieStateUpdateCoordinator
| TessieBatteryHealthCoordinator
| TessieEnergySiteInfoCoordinator
| TessieEnergySiteLiveCoordinator
| TessieEnergyHistoryCoordinator,
@@ -139,6 +142,22 @@ class TessieEnergyEntity(TessieBaseEntity):
super().__init__(coordinator, key)
class TessieBatteryEntity(TessieBaseEntity):
"""Parent class for Tessie battery health entities."""
def __init__(
self,
vehicle: TessieVehicleData,
key: str,
) -> None:
"""Initialize common aspects of a Tessie battery health entity."""
self.vin = vehicle.vin
self._attr_unique_id = f"{vehicle.vin}-{key}"
self._attr_device_info = vehicle.device
super().__init__(vehicle.battery_coordinator, key)
class TessieEnergyHistoryEntity(TessieBaseEntity):
"""Parent class for Tessie energy site history entities."""

View File

@@ -211,6 +211,9 @@
"energy_left": {
"default": "mdi:battery"
},
"energy_remaining": {
"default": "mdi:battery-medium"
},
"generator_power": {
"default": "mdi:generator-stationary"
},
@@ -220,9 +223,27 @@
"grid_services_power": {
"default": "mdi:transmission-tower"
},
"lifetime_energy_used": {
"default": "mdi:battery-heart-variant"
},
"load_power": {
"default": "mdi:power-plug"
},
"module_temp_max": {
"default": "mdi:thermometer-high"
},
"module_temp_min": {
"default": "mdi:thermometer-low"
},
"pack_current": {
"default": "mdi:current-dc"
},
"pack_voltage": {
"default": "mdi:lightning-bolt"
},
"phantom_drain_percent": {
"default": "mdi:battery-minus-outline"
},
"solar_power": {
"default": "mdi:solar-power"
},

View File

@@ -9,6 +9,7 @@ from tesla_fleet_api.tessie import EnergySite
from homeassistant.helpers.device_registry import DeviceInfo
from .coordinator import (
TessieBatteryHealthCoordinator,
TessieEnergyHistoryCoordinator,
TessieEnergySiteInfoCoordinator,
TessieEnergySiteLiveCoordinator,
@@ -41,5 +42,6 @@ class TessieVehicleData:
"""Data for a Tessie vehicle."""
data_coordinator: TessieStateUpdateCoordinator
battery_coordinator: TessieBatteryHealthCoordinator
device: DeviceInfo
vin: str

View File

@@ -36,6 +36,7 @@ from homeassistant.util.variance import ignore_variance
from . import TessieConfigEntry
from .const import ENERGY_HISTORY_FIELDS, TessieChargeStates, TessieWallConnectorStates
from .entity import (
TessieBatteryEntity,
TessieEnergyEntity,
TessieEnergyHistoryEntity,
TessieEntity,
@@ -272,6 +273,64 @@ DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = (
)
BATTERY_DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = (
TessieSensorEntityDescription(
key="phantom_drain_percent",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_display_precision=2,
),
TessieSensorEntityDescription(
key="energy_remaining",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY_STORAGE,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_display_precision=1,
),
TessieSensorEntityDescription(
key="lifetime_energy_used",
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_display_precision=1,
),
TessieSensorEntityDescription(
key="pack_current",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_display_precision=1,
),
TessieSensorEntityDescription(
key="pack_voltage",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_display_precision=1,
),
TessieSensorEntityDescription(
key="module_temp_min",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_display_precision=1,
),
TessieSensorEntityDescription(
key="module_temp_max",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_display_precision=1,
),
)
ENERGY_LIVE_DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = (
TessieSensorEntityDescription(
key="solar_power",
@@ -425,6 +484,12 @@ async def async_setup_entry(
for vehicle in entry.runtime_data.vehicles
for description in DESCRIPTIONS
),
( # Add vehicle battery health
TessieBatteryHealthSensorEntity(vehicle, description)
for vehicle in entry.runtime_data.vehicles
for description in BATTERY_DESCRIPTIONS
if description.key in vehicle.battery_coordinator.data
),
( # Add energy site info
TessieEnergyInfoSensorEntity(energysite, description)
for energysite in entry.runtime_data.energysites
@@ -483,6 +548,25 @@ class TessieVehicleSensorEntity(TessieEntity, SensorEntity):
return super().available and self.entity_description.available_fn(self.get())
class TessieBatteryHealthSensorEntity(TessieBatteryEntity, SensorEntity):
"""Sensor entity for Tessie battery health data."""
entity_description: TessieSensorEntityDescription
def __init__(
self,
vehicle: TessieVehicleData,
description: TessieSensorEntityDescription,
) -> None:
"""Initialize the sensor."""
self.entity_description = description
super().__init__(vehicle, description.key)
def _async_update_attrs(self) -> None:
"""Update the attributes of the sensor."""
self._attr_native_value = self.entity_description.value_fn(self._value)
class TessieEnergyLiveSensorEntity(TessieEnergyEntity, SensorEntity):
"""Base class for Tessie energy site sensor entity."""

View File

@@ -447,6 +447,9 @@
"energy_left": {
"name": "Energy left"
},
"energy_remaining": {
"name": "Energy remaining"
},
"generator_energy_exported": {
"name": "Generator exported"
},
@@ -487,12 +490,30 @@
"on_grid": "On-grid"
}
},
"lifetime_energy_used": {
"name": "Lifetime energy used"
},
"load_power": {
"name": "Load power"
},
"module_temp_max": {
"name": "Battery module temperature max"
},
"module_temp_min": {
"name": "Battery module temperature min"
},
"pack_current": {
"name": "Battery pack current"
},
"pack_voltage": {
"name": "Battery pack voltage"
},
"percentage_charged": {
"name": "Percentage charged"
},
"phantom_drain_percent": {
"name": "Phantom drain"
},
"solar_energy_exported": {
"name": "Solar exported"
},

View File

@@ -19,6 +19,7 @@ from tests.common import MockConfigEntry, load_json_object_fixture
# Tessie library
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_BATTERY = load_json_object_fixture("battery.json", DOMAIN)
TEST_VEHICLE_STATUS_AWAKE = {"status": TessieStatus.AWAKE}
TEST_VEHICLE_STATUS_ASLEEP = {"status": TessieStatus.ASLEEP}

View File

@@ -15,6 +15,7 @@ from .common import (
SCOPES,
SITE_INFO,
TEST_STATE_OF_ALL_VEHICLES,
TEST_VEHICLE_BATTERY,
TEST_VEHICLE_STATE_ONLINE,
TEST_VEHICLE_STATUS_AWAKE,
)
@@ -42,6 +43,22 @@ def mock_get_status():
yield mock_get_status
@pytest.fixture(autouse=True)
def mock_get_battery():
"""Mock get_battery function."""
with (
patch(
"homeassistant.components.tessie.get_battery",
return_value=TEST_VEHICLE_BATTERY,
) as mock_get_battery,
patch(
"homeassistant.components.tessie.coordinator.get_battery",
new=mock_get_battery,
),
):
yield mock_get_battery
@pytest.fixture(autouse=True)
def mock_get_state_of_all_vehicles():
"""Mock get_state_of_all_vehicles function."""

View File

@@ -0,0 +1,13 @@
{
"timestamp": 1704067200,
"battery_level": 73,
"battery_range": 250.5,
"ideal_battery_range": 280.2,
"phantom_drain_percent": 0.5,
"energy_remaining": 55.2,
"lifetime_energy_used": 12345.6,
"pack_current": -0.6,
"pack_voltage": 390.1,
"module_temp_min": 22.5,
"module_temp_max": 24
}

View File

@@ -161,6 +161,19 @@
]),
'vehicles': list([
dict({
'battery': dict({
'battery_level': 73,
'battery_range': 250.5,
'energy_remaining': 55.2,
'ideal_battery_range': 280.2,
'lifetime_energy_used': 12345.6,
'module_temp_max': 24,
'module_temp_min': 22.5,
'pack_current': -0.6,
'pack_voltage': 390.1,
'phantom_drain_percent': 0.5,
'timestamp': 1704067200,
}),
'data': dict({
'access_type': 'OWNER',
'api_version': 67,

View File

@@ -1986,6 +1986,234 @@
'state': '75',
})
# ---
# name: test_sensors[sensor.test_battery_module_temperature_max-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.test_battery_module_temperature_max',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Battery module temperature max',
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Battery module temperature max',
'platform': 'tessie',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'module_temp_max',
'unique_id': 'VINVINVIN-module_temp_max',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensors[sensor.test_battery_module_temperature_max-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Test Battery module temperature max',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_battery_module_temperature_max',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '24',
})
# ---
# name: test_sensors[sensor.test_battery_module_temperature_min-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.test_battery_module_temperature_min',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Battery module temperature min',
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Battery module temperature min',
'platform': 'tessie',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'module_temp_min',
'unique_id': 'VINVINVIN-module_temp_min',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_sensors[sensor.test_battery_module_temperature_min-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Test Battery module temperature min',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_battery_module_temperature_min',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '22.5',
})
# ---
# name: test_sensors[sensor.test_battery_pack_current-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.test_battery_pack_current',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Battery pack current',
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
'original_icon': None,
'original_name': 'Battery pack current',
'platform': 'tessie',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'pack_current',
'unique_id': 'VINVINVIN-pack_current',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
})
# ---
# name: test_sensors[sensor.test_battery_pack_current-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'current',
'friendly_name': 'Test Battery pack current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_battery_pack_current',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '-0.6',
})
# ---
# name: test_sensors[sensor.test_battery_pack_voltage-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.test_battery_pack_voltage',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Battery pack voltage',
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.VOLTAGE: 'voltage'>,
'original_icon': None,
'original_name': 'Battery pack voltage',
'platform': 'tessie',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'pack_voltage',
'unique_id': 'VINVINVIN-pack_voltage',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
})
# ---
# name: test_sensors[sensor.test_battery_pack_voltage-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'voltage',
'friendly_name': 'Test Battery pack voltage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_battery_pack_voltage',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '390.1',
})
# ---
# name: test_sensors[sensor.test_battery_range-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -2744,6 +2972,63 @@
'state': '46.92',
})
# ---
# name: test_sensors[sensor.test_energy_remaining_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.test_energy_remaining_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Energy remaining',
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY_STORAGE: 'energy_storage'>,
'original_icon': None,
'original_name': 'Energy remaining',
'platform': 'tessie',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'energy_remaining',
'unique_id': 'VINVINVIN-energy_remaining',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_sensors[sensor.test_energy_remaining_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy_storage',
'friendly_name': 'Test Energy remaining',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_energy_remaining_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '55.2',
})
# ---
# name: test_sensors[sensor.test_inside_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -2801,6 +3086,63 @@
'state': '30.4',
})
# ---
# name: test_sensors[sensor.test_lifetime_energy_used-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.test_lifetime_energy_used',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Lifetime energy used',
'options': dict({
'sensor': dict({
'suggested_display_precision': 1,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Lifetime energy used',
'platform': 'tessie',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'lifetime_energy_used',
'unique_id': 'VINVINVIN-lifetime_energy_used',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_sensors[sensor.test_lifetime_energy_used-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': 'Test Lifetime energy used',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_lifetime_energy_used',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '12345.6',
})
# ---
# name: test_sensors[sensor.test_odometer-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -2975,6 +3317,62 @@
'state': '22.5',
})
# ---
# name: test_sensors[sensor.test_phantom_drain-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.test_phantom_drain',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Phantom drain',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Phantom drain',
'platform': 'tessie',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'phantom_drain_percent',
'unique_id': 'VINVINVIN-phantom_drain_percent',
'unit_of_measurement': '%',
})
# ---
# name: test_sensors[sensor.test_phantom_drain-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test Phantom drain',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.test_phantom_drain',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.5',
})
# ---
# name: test_sensors[sensor.test_power-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -102,6 +102,51 @@ async def test_coordinator_connection(
assert hass.states.get("binary_sensor.test_status").state == STATE_UNAVAILABLE
async def test_coordinator_battery_update(
hass: HomeAssistant, mock_get_battery, freezer: FrozenDateTimeFactory
) -> None:
"""Tests that the battery coordinator handles updates."""
await setup_platform(hass, [Platform.SENSOR])
mock_get_battery.reset_mock()
freezer.tick(WAIT)
async_fire_time_changed(hass)
await hass.async_block_till_done()
mock_get_battery.assert_called_once()
async def test_coordinator_battery_auth(
hass: HomeAssistant, mock_get_battery, freezer: FrozenDateTimeFactory
) -> None:
"""Tests that the battery coordinator handles auth errors."""
await setup_platform(hass, [Platform.SENSOR])
mock_get_battery.reset_mock()
mock_get_battery.side_effect = ERROR_AUTH
freezer.tick(WAIT)
async_fire_time_changed(hass)
await hass.async_block_till_done()
mock_get_battery.assert_called_once()
async def test_coordinator_battery_error(
hass: HomeAssistant, mock_get_battery, freezer: FrozenDateTimeFactory
) -> None:
"""Tests that the battery coordinator handles client errors."""
await setup_platform(hass, [Platform.SENSOR])
mock_get_battery.reset_mock()
mock_get_battery.side_effect = ERROR_UNKNOWN
freezer.tick(WAIT)
async_fire_time_changed(hass)
await hass.async_block_till_done()
mock_get_battery.assert_called_once()
assert hass.states.get("sensor.test_phantom_drain").state == STATE_UNAVAILABLE
async def test_coordinator_live_error(
hass: HomeAssistant, mock_live_status, freezer: FrozenDateTimeFactory
) -> None:

View File

@@ -2,6 +2,7 @@
from unittest.mock import patch
import pytest
from tesla_fleet_api.exceptions import TeslaFleetError
from homeassistant.config_entries import ConfigEntryState
@@ -50,6 +51,27 @@ async def test_connection_failure(
assert entry.state is ConfigEntryState.SETUP_RETRY
@pytest.mark.parametrize(
("side_effect", "expected_state"),
[
(ERROR_AUTH, ConfigEntryState.SETUP_ERROR),
(ERROR_UNKNOWN, ConfigEntryState.SETUP_ERROR),
(ERROR_CONNECTION, ConfigEntryState.SETUP_RETRY),
],
)
async def test_battery_setup_failure(
hass: HomeAssistant,
mock_get_battery,
side_effect: Exception,
expected_state: ConfigEntryState,
) -> None:
"""Test init with a battery API error."""
mock_get_battery.side_effect = side_effect
entry = await setup_platform(hass)
assert entry.state is expected_state
async def test_products_error(hass: HomeAssistant) -> None:
"""Test init with a fleet error on products."""