"""Sensor platform for Tessie integration.""" from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta from itertools import chain from typing import cast from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) from homeassistant.const import ( PERCENTAGE, EntityCategory, UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, UnitOfLength, UnitOfPower, UnitOfPressure, UnitOfSpeed, UnitOfTemperature, UnitOfTime, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util import dt as dt_util from homeassistant.util.variance import ignore_variance from . import TessieConfigEntry from .const import TessieChargeStates, TessieWallConnectorStates from .entity import TessieEnergyEntity, TessieEntity, TessieWallConnectorEntity from .models import TessieEnergyData, TessieVehicleData @callback def minutes_to_datetime(value: StateType) -> datetime | None: """Convert relative minutes into absolute datetime.""" if isinstance(value, (int, float)) and value > 0: return dt_util.now() + timedelta(minutes=value) return None @dataclass(frozen=True, kw_only=True) class TessieSensorEntityDescription(SensorEntityDescription): """Describes Tessie Sensor entity.""" value_fn: Callable[[StateType], StateType | datetime] = lambda x: x available_fn: Callable[[StateType], bool] = lambda _: True DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( TessieSensorEntityDescription( key="charge_state_charging_state", options=list(TessieChargeStates.values()), device_class=SensorDeviceClass.ENUM, value_fn=lambda value: TessieChargeStates[cast(str, value)], ), TessieSensorEntityDescription( key="charge_state_usable_battery_level", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, ), TessieSensorEntityDescription( key="charge_state_charge_energy_added", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, suggested_display_precision=1, ), TessieSensorEntityDescription( key="charge_state_charger_power", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, device_class=SensorDeviceClass.POWER, ), TessieSensorEntityDescription( key="charge_state_charger_voltage", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, entity_category=EntityCategory.DIAGNOSTIC, ), TessieSensorEntityDescription( key="charge_state_charger_actual_current", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, entity_category=EntityCategory.DIAGNOSTIC, ), TessieSensorEntityDescription( key="charge_state_charge_rate", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, device_class=SensorDeviceClass.SPEED, entity_category=EntityCategory.DIAGNOSTIC, ), TessieSensorEntityDescription( key="charge_state_minutes_to_full_charge", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, value_fn=minutes_to_datetime, ), TessieSensorEntityDescription( key="charge_state_battery_range", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfLength.MILES, device_class=SensorDeviceClass.DISTANCE, suggested_display_precision=1, ), TessieSensorEntityDescription( key="charge_state_est_battery_range", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfLength.MILES, device_class=SensorDeviceClass.DISTANCE, suggested_display_precision=1, entity_registry_enabled_default=False, ), TessieSensorEntityDescription( key="charge_state_ideal_battery_range", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfLength.MILES, device_class=SensorDeviceClass.DISTANCE, suggested_display_precision=1, entity_registry_enabled_default=False, ), TessieSensorEntityDescription( key="drive_state_speed", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, device_class=SensorDeviceClass.SPEED, ), TessieSensorEntityDescription( key="drive_state_power", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, device_class=SensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, ), TessieSensorEntityDescription( key="drive_state_shift_state", options=["p", "d", "r", "n"], device_class=SensorDeviceClass.ENUM, value_fn=lambda x: x.lower() if isinstance(x, str) else x, ), TessieSensorEntityDescription( key="vehicle_state_odometer", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfLength.MILES, device_class=SensorDeviceClass.DISTANCE, suggested_display_precision=0, entity_category=EntityCategory.DIAGNOSTIC, ), TessieSensorEntityDescription( key="vehicle_state_tpms_pressure_fl", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.BAR, suggested_unit_of_measurement=UnitOfPressure.PSI, device_class=SensorDeviceClass.PRESSURE, suggested_display_precision=1, entity_category=EntityCategory.DIAGNOSTIC, ), TessieSensorEntityDescription( key="vehicle_state_tpms_pressure_fr", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.BAR, suggested_unit_of_measurement=UnitOfPressure.PSI, device_class=SensorDeviceClass.PRESSURE, suggested_display_precision=1, entity_category=EntityCategory.DIAGNOSTIC, ), TessieSensorEntityDescription( key="vehicle_state_tpms_pressure_rl", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.BAR, suggested_unit_of_measurement=UnitOfPressure.PSI, device_class=SensorDeviceClass.PRESSURE, suggested_display_precision=1, entity_category=EntityCategory.DIAGNOSTIC, ), TessieSensorEntityDescription( key="vehicle_state_tpms_pressure_rr", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.BAR, suggested_unit_of_measurement=UnitOfPressure.PSI, device_class=SensorDeviceClass.PRESSURE, suggested_display_precision=1, entity_category=EntityCategory.DIAGNOSTIC, ), TessieSensorEntityDescription( key="climate_state_inside_temp", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, suggested_display_precision=1, ), TessieSensorEntityDescription( key="climate_state_outside_temp", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, suggested_display_precision=1, ), TessieSensorEntityDescription( key="climate_state_driver_temp_setting", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, suggested_display_precision=1, entity_category=EntityCategory.DIAGNOSTIC, ), TessieSensorEntityDescription( key="climate_state_passenger_temp_setting", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, suggested_display_precision=1, entity_category=EntityCategory.DIAGNOSTIC, ), TessieSensorEntityDescription( key="drive_state_active_route_traffic_minutes_delay", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTime.MINUTES, device_class=SensorDeviceClass.DURATION, ), TessieSensorEntityDescription( key="drive_state_active_route_energy_at_arrival", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, ), TessieSensorEntityDescription( key="drive_state_active_route_miles_to_arrival", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfLength.MILES, device_class=SensorDeviceClass.DISTANCE, ), TessieSensorEntityDescription( key="drive_state_active_route_minutes_to_arrival", device_class=SensorDeviceClass.TIMESTAMP, value_fn=ignore_variance( lambda value: dt_util.now() + timedelta(minutes=cast(float, value)), timedelta(seconds=30), ), available_fn=lambda x: x is not None, ), TessieSensorEntityDescription( key="drive_state_active_route_destination", entity_category=EntityCategory.DIAGNOSTIC, ), ) ENERGY_LIVE_DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( TessieSensorEntityDescription( key="solar_power", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, suggested_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=2, device_class=SensorDeviceClass.POWER, ), TessieSensorEntityDescription( key="energy_left", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=2, device_class=SensorDeviceClass.ENERGY_STORAGE, entity_category=EntityCategory.DIAGNOSTIC, ), TessieSensorEntityDescription( key="total_pack_energy", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=2, device_class=SensorDeviceClass.ENERGY_STORAGE, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), TessieSensorEntityDescription( key="percentage_charged", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, suggested_display_precision=2, ), TessieSensorEntityDescription( key="battery_power", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, suggested_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=2, device_class=SensorDeviceClass.POWER, ), TessieSensorEntityDescription( key="load_power", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, suggested_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=2, device_class=SensorDeviceClass.POWER, ), TessieSensorEntityDescription( key="grid_power", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, suggested_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=2, device_class=SensorDeviceClass.POWER, ), TessieSensorEntityDescription( key="grid_services_power", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, suggested_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=2, device_class=SensorDeviceClass.POWER, ), TessieSensorEntityDescription( key="generator_power", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, suggested_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=2, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, ), ) WALL_CONNECTOR_DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( TessieSensorEntityDescription( key="wall_connector_state", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, device_class=SensorDeviceClass.ENUM, value_fn=lambda x: TessieWallConnectorStates(cast(int, x)).name.lower(), options=[state.name.lower() for state in TessieWallConnectorStates], ), TessieSensorEntityDescription( key="wall_connector_power", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, suggested_unit_of_measurement=UnitOfPower.KILO_WATT, suggested_display_precision=2, device_class=SensorDeviceClass.POWER, ), TessieSensorEntityDescription( key="vin", ), ) ENERGY_INFO_DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = ( TessieSensorEntityDescription( key="vpp_backup_reserve_percent", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, ), ) PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, entry: TessieConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Tessie sensor platform from a config entry.""" async_add_entities( chain( ( # Add vehicles TessieVehicleSensorEntity(vehicle, description) for vehicle in entry.runtime_data.vehicles for description in DESCRIPTIONS ), ( # Add energy site info TessieEnergyInfoSensorEntity(energysite, description) for energysite in entry.runtime_data.energysites for description in ENERGY_INFO_DESCRIPTIONS if description.key in energysite.info_coordinator.data ), ( # Add energy site live TessieEnergyLiveSensorEntity(energysite, description) for energysite in entry.runtime_data.energysites for description in ENERGY_LIVE_DESCRIPTIONS if description.key in energysite.live_coordinator.data ), ( # Add wall connectors TessieWallConnectorSensorEntity(energysite, din, description) for energysite in entry.runtime_data.energysites for din in energysite.live_coordinator.data.get("wall_connectors", {}) for description in WALL_CONNECTOR_DESCRIPTIONS ), ) ) class TessieVehicleSensorEntity(TessieEntity, SensorEntity): """Base class for Tessie sensor entities.""" entity_description: TessieSensorEntityDescription def __init__( self, vehicle: TessieVehicleData, description: TessieSensorEntityDescription, ) -> None: """Initialize the sensor.""" self.entity_description = description super().__init__(vehicle, description.key) @property def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" return self.entity_description.value_fn(self.get()) @property def available(self) -> bool: """Return if sensor is available.""" return super().available and self.entity_description.available_fn(self.get()) class TessieEnergyLiveSensorEntity(TessieEnergyEntity, SensorEntity): """Base class for Tessie energy site sensor entity.""" entity_description: TessieSensorEntityDescription def __init__( self, data: TessieEnergyData, description: TessieSensorEntityDescription, ) -> None: """Initialize the sensor.""" self.entity_description = description super().__init__(data, data.live_coordinator, description.key) def _async_update_attrs(self) -> None: """Update the attributes of the sensor.""" self._attr_available = self._value is not None self._attr_native_value = self.entity_description.value_fn(self._value) class TessieEnergyInfoSensorEntity(TessieEnergyEntity, SensorEntity): """Base class for Tessie energy site sensor entity.""" entity_description: TessieSensorEntityDescription def __init__( self, data: TessieEnergyData, description: TessieSensorEntityDescription, ) -> None: """Initialize the sensor.""" self.entity_description = description super().__init__(data, data.info_coordinator, description.key) def _async_update_attrs(self) -> None: """Update the attributes of the sensor.""" self._attr_available = self._value is not None self._attr_native_value = self._value class TessieWallConnectorSensorEntity(TessieWallConnectorEntity, SensorEntity): """Base class for Tessie wall connector sensor entity.""" entity_description: TessieSensorEntityDescription def __init__( self, data: TessieEnergyData, din: str, description: TessieSensorEntityDescription, ) -> None: """Initialize the sensor.""" self.entity_description = description super().__init__( data, din, description.key, ) def _async_update_attrs(self) -> None: """Update the attributes of the sensor.""" self._attr_available = self._value is not None self._attr_native_value = self.entity_description.value_fn(self._value)