1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-30 12:14:20 +01:00
Files
2026-04-30 21:14:48 +02:00

640 lines
24 KiB
Python

"""Sensor platform for Tessie integration."""
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 AddConfigEntryEntitiesCallback
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 (
ENERGY_HISTORY_FIELDS,
TessieChargePortLatchStates,
TessieChargeStates,
TessieWallConnectorStates,
)
from .entity import (
TessieEnergyEntity,
TessieEnergyHistoryEntity,
TessieEntity,
TessieWallConnectorEntity,
)
from .helpers import charge_state_to_option
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."""
data_key: str | None = None
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=charge_state_to_option,
),
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="phantom_drain_percent",
data_key="charge_state_phantom_drain",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_display_precision=2,
),
TessieSensorEntityDescription(
key="charge_state_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=2,
),
TessieSensorEntityDescription(
key="lifetime_energy_used",
data_key="charge_state_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",
data_key="charge_state_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",
data_key="charge_state_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",
data_key="charge_state_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",
data_key="charge_state_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,
),
TessieSensorEntityDescription(
key="charge_state_conn_charge_cable",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
TessieSensorEntityDescription(
key="charge_state_charge_port_latch",
options=list(TessieChargePortLatchStates.values()),
device_class=SensorDeviceClass.ENUM,
value_fn=lambda value: TessieChargePortLatchStates[cast(str, value)],
entity_category=EntityCategory.DIAGNOSTIC,
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 "p",
),
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,
value_fn=lambda value: value or 0,
),
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,
),
TessieSensorEntityDescription(
key="island_status",
device_class=SensorDeviceClass.ENUM,
options=[
"on_grid",
"off_grid",
"off_grid_intentional",
"off_grid_unintentional",
"island_status_unknown",
],
),
)
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,
native_unit_of_measurement=PERCENTAGE,
),
)
ENERGY_HISTORY_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = tuple(
SensorEntityDescription(
key=key,
translation_key=key,
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
suggested_display_precision=2,
state_class=SensorStateClass.TOTAL,
entity_registry_enabled_default=(
key.startswith("total") or key == "grid_energy_imported"
),
)
for key in ENERGY_HISTORY_FIELDS
)
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
entry: TessieConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> 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 energysite.live_coordinator is not None
and (
description.key in energysite.live_coordinator.data
or description.key == "percentage_charged"
)
),
( # Add wall connectors
TessieWallConnectorSensorEntity(energysite, din, description)
for energysite in entry.runtime_data.energysites
if energysite.live_coordinator is not None
for din in energysite.live_coordinator.data.get("wall_connectors", {})
for description in WALL_CONNECTOR_DESCRIPTIONS
),
( # Add energy history
TessieEnergyHistorySensorEntity(energysite, description)
for energysite in entry.runtime_data.energysites
for description in ENERGY_HISTORY_DESCRIPTIONS
if energysite.history_coordinator is not None
),
)
)
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, description.data_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
assert data.live_coordinator is not None
super().__init__(data, data.live_coordinator, 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 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)
class TessieEnergyHistorySensorEntity(TessieEnergyHistoryEntity, SensorEntity):
"""Sensor entity for Tessie energy site history."""
entity_description: SensorEntityDescription
def __init__(
self,
data: TessieEnergyData,
description: SensorEntityDescription,
) -> None:
"""Initialize the sensor."""
self.entity_description = description
super().__init__(data, 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
self._attr_last_reset = self.coordinator.data.get("_period_start")