"""Sensor platform for Teslemetry 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.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, EntityCategory, UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, UnitOfLength, UnitOfPower, UnitOfPressure, UnitOfSpeed, UnitOfTemperature, UnitOfTime, ) from homeassistant.core import HomeAssistant 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 .const import DOMAIN from .entity import ( TeslemetryEnergyEntity, TeslemetryVehicleEntity, TeslemetryWallConnectorEntity, ) from .models import TeslemetryEnergyData, TeslemetryVehicleData CHARGE_STATES = { "Starting": "starting", "Charging": "charging", "Stopped": "stopped", "Complete": "complete", "Disconnected": "disconnected", "NoPower": "no_power", } SHIFT_STATES = {"P": "p", "D": "d", "R": "r", "N": "n"} @dataclass(frozen=True, kw_only=True) class TeslemetrySensorEntityDescription(SensorEntityDescription): """Describes Teslemetry Sensor entity.""" value_fn: Callable[[StateType], StateType | datetime] = lambda x: x VEHICLE_DESCRIPTIONS: tuple[TeslemetrySensorEntityDescription, ...] = ( TeslemetrySensorEntityDescription( key="charge_state_charging_state", options=list(CHARGE_STATES.values()), device_class=SensorDeviceClass.ENUM, value_fn=lambda value: CHARGE_STATES.get(cast(str, value)), ), TeslemetrySensorEntityDescription( key="charge_state_battery_level", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, ), TeslemetrySensorEntityDescription( key="charge_state_usable_battery_level", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, entity_registry_enabled_default=False, ), TeslemetrySensorEntityDescription( 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, ), TeslemetrySensorEntityDescription( key="charge_state_charger_power", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, device_class=SensorDeviceClass.POWER, ), TeslemetrySensorEntityDescription( key="charge_state_charger_voltage", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, entity_category=EntityCategory.DIAGNOSTIC, ), TeslemetrySensorEntityDescription( key="charge_state_charger_actual_current", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, entity_category=EntityCategory.DIAGNOSTIC, ), TeslemetrySensorEntityDescription( 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, ), TeslemetrySensorEntityDescription( key="charge_state_conn_charge_cable", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), TeslemetrySensorEntityDescription( key="charge_state_fast_charger_type", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), TeslemetrySensorEntityDescription( key="charge_state_battery_range", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfLength.MILES, device_class=SensorDeviceClass.DISTANCE, suggested_display_precision=1, ), TeslemetrySensorEntityDescription( 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, ), TeslemetrySensorEntityDescription( 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, ), TeslemetrySensorEntityDescription( key="drive_state_speed", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, device_class=SensorDeviceClass.SPEED, entity_registry_enabled_default=False, value_fn=lambda value: value or 0, ), TeslemetrySensorEntityDescription( key="drive_state_power", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.KILO_WATT, device_class=SensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda value: value or 0, ), TeslemetrySensorEntityDescription( key="drive_state_shift_state", options=list(SHIFT_STATES.values()), device_class=SensorDeviceClass.ENUM, value_fn=lambda x: SHIFT_STATES.get(str(x), "p"), entity_registry_enabled_default=False, ), TeslemetrySensorEntityDescription( 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, entity_registry_enabled_default=False, ), TeslemetrySensorEntityDescription( 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, entity_registry_enabled_default=False, ), TeslemetrySensorEntityDescription( 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, entity_registry_enabled_default=False, ), TeslemetrySensorEntityDescription( 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, entity_registry_enabled_default=False, ), TeslemetrySensorEntityDescription( 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, entity_registry_enabled_default=False, ), TeslemetrySensorEntityDescription( key="climate_state_inside_temp", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, suggested_display_precision=1, ), TeslemetrySensorEntityDescription( key="climate_state_outside_temp", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, suggested_display_precision=1, ), TeslemetrySensorEntityDescription( 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, entity_registry_enabled_default=False, ), TeslemetrySensorEntityDescription( 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, entity_registry_enabled_default=False, ), TeslemetrySensorEntityDescription( key="drive_state_active_route_traffic_minutes_delay", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTime.MINUTES, device_class=SensorDeviceClass.DURATION, entity_registry_enabled_default=False, ), TeslemetrySensorEntityDescription( 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, entity_registry_enabled_default=False, ), TeslemetrySensorEntityDescription( key="drive_state_active_route_miles_to_arrival", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfLength.MILES, device_class=SensorDeviceClass.DISTANCE, ), ) @dataclass(frozen=True, kw_only=True) class TeslemetryTimeEntityDescription(SensorEntityDescription): """Describes Teslemetry Sensor entity.""" variance: int VEHICLE_TIME_DESCRIPTIONS: tuple[TeslemetryTimeEntityDescription, ...] = ( TeslemetryTimeEntityDescription( key="charge_state_minutes_to_full_charge", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, variance=4, ), TeslemetryTimeEntityDescription( key="drive_state_active_route_minutes_to_arrival", device_class=SensorDeviceClass.TIMESTAMP, variance=1, ), ) ENERGY_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( 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, ), SensorEntityDescription( 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, ), SensorEntityDescription( 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, ), SensorEntityDescription( key="percentage_charged", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, suggested_display_precision=2, ), SensorEntityDescription( 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, ), SensorEntityDescription( 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, ), SensorEntityDescription( 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, ), SensorEntityDescription( 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, ), SensorEntityDescription( 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, ), SensorEntityDescription(key="island_status", device_class=SensorDeviceClass.ENUM), ) WALL_CONNECTOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="wall_connector_state", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), SensorEntityDescription( key="wall_connector_fault_state", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), SensorEntityDescription( 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, ), SensorEntityDescription( key="vin", ), ) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Teslemetry sensor platform from a config entry.""" data = hass.data[DOMAIN][entry.entry_id] async_add_entities( chain( ( # Add vehicles TeslemetryVehicleSensorEntity(vehicle, description) for vehicle in data.vehicles for description in VEHICLE_DESCRIPTIONS ), ( # Add vehicles time sensors TeslemetryVehicleTimeSensorEntity(vehicle, description) for vehicle in data.vehicles for description in VEHICLE_TIME_DESCRIPTIONS ), ( # Add energy site live TeslemetryEnergySensorEntity(energysite, description) for energysite in data.energysites for description in ENERGY_DESCRIPTIONS if description.key in energysite.coordinator.data ), ( # Add wall connectors TeslemetryWallConnectorSensorEntity(energysite, din, description) for energysite in data.energysites for din in energysite.coordinator.data.get("wall_connectors", {}) for description in WALL_CONNECTOR_DESCRIPTIONS ), ) ) class TeslemetryVehicleSensorEntity(TeslemetryVehicleEntity, SensorEntity): """Base class for Teslemetry vehicle metric sensors.""" entity_description: TeslemetrySensorEntityDescription def __init__( self, vehicle: TeslemetryVehicleData, description: TeslemetrySensorEntityDescription, ) -> None: """Initialize the sensor.""" super().__init__(vehicle, description.key) class TeslemetryVehicleTimeSensorEntity(TeslemetryVehicleEntity, SensorEntity): """Base class for Teslemetry vehicle metric sensors.""" entity_description: TeslemetryTimeEntityDescription def __init__( self, data: TeslemetryVehicleData, description: TeslemetryTimeEntityDescription, ) -> None: """Initialize the sensor.""" self.entity_description = description self._get_timestamp = ignore_variance( func=lambda value: dt_util.now() + timedelta(minutes=value), ignored_variance=timedelta(minutes=description.variance), ) super().__init__(data, description.key) @property def native_value(self) -> datetime | None: """Return the state of the sensor.""" return self._get_timestamp(self._value) @property def available(self) -> bool: """Return the avaliability of the sensor.""" return isinstance(self._value, int | float) and self._value > 0 class TeslemetryEnergySensorEntity(TeslemetryEnergyEntity, SensorEntity): """Base class for Teslemetry energy site metric sensors.""" entity_description: SensorEntityDescription def __init__( self, energysite: TeslemetryEnergyData, description: SensorEntityDescription, ) -> None: """Initialize the sensor.""" super().__init__(energysite, description.key) self.entity_description = description @property def native_value(self) -> StateType: """Return the state of the sensor.""" return self.get() class TeslemetryWallConnectorSensorEntity(TeslemetryWallConnectorEntity, SensorEntity): """Base class for Teslemetry energy site metric sensors.""" entity_description: SensorEntityDescription def __init__( self, energysite: TeslemetryEnergyData, din: str, description: SensorEntityDescription, ) -> None: """Initialize the sensor.""" super().__init__( energysite, din, description.key, ) self.entity_description = description @property def native_value(self) -> StateType: """Return the state of the sensor.""" return self._value