mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 03:03:17 +01:00
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:
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
13
tests/components/tessie/fixtures/battery.json
Normal file
13
tests/components/tessie/fixtures/battery.json
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user