From 8d228b6e6a3c026d092c05172f4e362523ce105e Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 17 Feb 2026 00:57:05 +1000 Subject: [PATCH] Add battery health sensors to Tessie (#162908) Co-authored-by: Claude Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Joostlek --- homeassistant/components/tessie/__init__.py | 49 ++- .../components/tessie/coordinator.py | 44 +- .../components/tessie/diagnostics.py | 2 +- homeassistant/components/tessie/entity.py | 19 + homeassistant/components/tessie/icons.json | 21 + homeassistant/components/tessie/models.py | 2 + homeassistant/components/tessie/sensor.py | 84 ++++ homeassistant/components/tessie/strings.json | 21 + tests/components/tessie/common.py | 1 + tests/components/tessie/conftest.py | 17 + tests/components/tessie/fixtures/battery.json | 13 + .../tessie/snapshots/test_diagnostics.ambr | 13 + .../tessie/snapshots/test_sensor.ambr | 398 ++++++++++++++++++ tests/components/tessie/test_coordinator.py | 45 ++ tests/components/tessie/test_init.py | 22 + 15 files changed, 743 insertions(+), 8 deletions(-) create mode 100644 tests/components/tessie/fixtures/battery.json diff --git a/homeassistant/components/tessie/__init__.py b/homeassistant/components/tessie/__init__.py index 41096ad167e..d2349fc4572 100644 --- a/homeassistant/components/tessie/__init__.py +++ b/homeassistant/components/tessie/__init__.py @@ -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 diff --git a/homeassistant/components/tessie/coordinator.py b/homeassistant/components/tessie/coordinator.py index ff2b7ff78d7..bb9f2a63734 100644 --- a/homeassistant/components/tessie/coordinator.py +++ b/homeassistant/components/tessie/coordinator.py @@ -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.""" diff --git a/homeassistant/components/tessie/diagnostics.py b/homeassistant/components/tessie/diagnostics.py index 21fc208612d..6d2daaccf78 100644 --- a/homeassistant/components/tessie/diagnostics.py +++ b/homeassistant/components/tessie/diagnostics.py @@ -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 ] diff --git a/homeassistant/components/tessie/entity.py b/homeassistant/components/tessie/entity.py index d4dec969f1c..a717fa5f06a 100644 --- a/homeassistant/components/tessie/entity.py +++ b/homeassistant/components/tessie/entity.py @@ -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.""" diff --git a/homeassistant/components/tessie/icons.json b/homeassistant/components/tessie/icons.json index 5a67cdffb5f..917cb258fd9 100644 --- a/homeassistant/components/tessie/icons.json +++ b/homeassistant/components/tessie/icons.json @@ -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" }, diff --git a/homeassistant/components/tessie/models.py b/homeassistant/components/tessie/models.py index e4e4bb34e81..7302071693b 100644 --- a/homeassistant/components/tessie/models.py +++ b/homeassistant/components/tessie/models.py @@ -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 diff --git a/homeassistant/components/tessie/sensor.py b/homeassistant/components/tessie/sensor.py index 54b8031197d..f512c1eeaaf 100644 --- a/homeassistant/components/tessie/sensor.py +++ b/homeassistant/components/tessie/sensor.py @@ -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.""" diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index c2f2a719397..35f22ac301a 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -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" }, diff --git a/tests/components/tessie/common.py b/tests/components/tessie/common.py index f1b1d8c1ba0..81f9bb97d9f 100644 --- a/tests/components/tessie/common.py +++ b/tests/components/tessie/common.py @@ -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} diff --git a/tests/components/tessie/conftest.py b/tests/components/tessie/conftest.py index 47a86c8b11f..217b4d1215c 100644 --- a/tests/components/tessie/conftest.py +++ b/tests/components/tessie/conftest.py @@ -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.""" diff --git a/tests/components/tessie/fixtures/battery.json b/tests/components/tessie/fixtures/battery.json new file mode 100644 index 00000000000..6acec073c7e --- /dev/null +++ b/tests/components/tessie/fixtures/battery.json @@ -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 +} diff --git a/tests/components/tessie/snapshots/test_diagnostics.ambr b/tests/components/tessie/snapshots/test_diagnostics.ambr index d89f035e3d7..9411e86007c 100644 --- a/tests/components/tessie/snapshots/test_diagnostics.ambr +++ b/tests/components/tessie/snapshots/test_diagnostics.ambr @@ -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, diff --git a/tests/components/tessie/snapshots/test_sensor.ambr b/tests/components/tessie/snapshots/test_sensor.ambr index df39cde7253..1bac7c86372 100644 --- a/tests/components/tessie/snapshots/test_sensor.ambr +++ b/tests/components/tessie/snapshots/test_sensor.ambr @@ -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': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_battery_module_temperature_max', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery module temperature max', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + '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': , + }) +# --- +# 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': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_battery_module_temperature_max', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24', + }) +# --- +# name: test_sensors[sensor.test_battery_module_temperature_min-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_battery_module_temperature_min', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery module temperature min', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + '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': , + }) +# --- +# 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': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_battery_module_temperature_min', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22.5', + }) +# --- +# name: test_sensors[sensor.test_battery_pack_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_battery_pack_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack current', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + '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': , + }) +# --- +# name: test_sensors[sensor.test_battery_pack_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Test Battery pack current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_battery_pack_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-0.6', + }) +# --- +# name: test_sensors[sensor.test_battery_pack_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_battery_pack_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Battery pack voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + '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': , + }) +# --- +# name: test_sensors[sensor.test_battery_pack_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Test Battery pack voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_battery_pack_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_energy_remaining_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Energy remaining', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + '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': , + }) +# --- +# name: test_sensors[sensor.test_energy_remaining_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy_storage', + 'friendly_name': 'Test Energy remaining', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_energy_remaining_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_lifetime_energy_used', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Lifetime energy used', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + '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': , + }) +# --- +# name: test_sensors[sensor.test_lifetime_energy_used-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Test Lifetime energy used', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_lifetime_energy_used', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_phantom_drain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + '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': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.test_phantom_drain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.5', + }) +# --- # name: test_sensors[sensor.test_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tessie/test_coordinator.py b/tests/components/tessie/test_coordinator.py index 44f82a7fb8e..414de14753e 100644 --- a/tests/components/tessie/test_coordinator.py +++ b/tests/components/tessie/test_coordinator.py @@ -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: diff --git a/tests/components/tessie/test_init.py b/tests/components/tessie/test_init.py index 921ef93b1ae..3e546bd63af 100644 --- a/tests/components/tessie/test_init.py +++ b/tests/components/tessie/test_init.py @@ -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."""