core/homeassistant/components/subaru/sensor.py

327 lines
10 KiB
Python

"""Support for Subaru sensors."""
from __future__ import annotations
import logging
from typing import Any
import subarulink.const as sc
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ELECTRIC_POTENTIAL_VOLT,
LENGTH_KILOMETERS,
LENGTH_MILES,
PERCENTAGE,
PRESSURE_HPA,
TEMP_CELSIUS,
VOLUME_GALLONS,
VOLUME_LITERS,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.util.unit_conversion import DistanceConverter, VolumeConverter
from homeassistant.util.unit_system import (
LENGTH_UNITS,
PRESSURE_UNITS,
US_CUSTOMARY_SYSTEM,
)
from . import get_device_info
from .const import (
API_GEN_2,
DOMAIN,
ENTRY_COORDINATOR,
ENTRY_VEHICLES,
VEHICLE_API_GEN,
VEHICLE_HAS_EV,
VEHICLE_HAS_SAFETY_SERVICE,
VEHICLE_STATUS,
VEHICLE_VIN,
)
_LOGGER = logging.getLogger(__name__)
# Fuel consumption units
FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS = "L/100km"
FUEL_CONSUMPTION_MILES_PER_GALLON = "mi/gal"
L_PER_GAL = VolumeConverter.convert(1, VOLUME_GALLONS, VOLUME_LITERS)
KM_PER_MI = DistanceConverter.convert(1, LENGTH_MILES, LENGTH_KILOMETERS)
# Sensor available to "Subaru Safety Plus" subscribers with Gen1 or Gen2 vehicles
SAFETY_SENSORS = [
SensorEntityDescription(
key=sc.ODOMETER,
device_class=SensorDeviceClass.DISTANCE,
icon="mdi:road-variant",
name="Odometer",
native_unit_of_measurement=LENGTH_KILOMETERS,
state_class=SensorStateClass.TOTAL_INCREASING,
),
]
# Sensors available to "Subaru Safety Plus" subscribers with Gen2 vehicles
API_GEN_2_SENSORS = [
SensorEntityDescription(
key=sc.AVG_FUEL_CONSUMPTION,
icon="mdi:leaf",
name="Avg fuel consumption",
native_unit_of_measurement=FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=sc.DIST_TO_EMPTY,
device_class=SensorDeviceClass.DISTANCE,
icon="mdi:gas-station",
name="Range",
native_unit_of_measurement=LENGTH_KILOMETERS,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=sc.TIRE_PRESSURE_FL,
device_class=SensorDeviceClass.PRESSURE,
name="Tire pressure FL",
native_unit_of_measurement=PRESSURE_HPA,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=sc.TIRE_PRESSURE_FR,
device_class=SensorDeviceClass.PRESSURE,
name="Tire pressure FR",
native_unit_of_measurement=PRESSURE_HPA,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=sc.TIRE_PRESSURE_RL,
device_class=SensorDeviceClass.PRESSURE,
name="Tire pressure RL",
native_unit_of_measurement=PRESSURE_HPA,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=sc.TIRE_PRESSURE_RR,
device_class=SensorDeviceClass.PRESSURE,
name="Tire pressure RR",
native_unit_of_measurement=PRESSURE_HPA,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=sc.EXTERNAL_TEMP,
device_class=SensorDeviceClass.TEMPERATURE,
name="External temp",
native_unit_of_measurement=TEMP_CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=sc.BATTERY_VOLTAGE,
device_class=SensorDeviceClass.VOLTAGE,
name="12V battery voltage",
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
state_class=SensorStateClass.MEASUREMENT,
),
]
# Sensors available to "Subaru Safety Plus" subscribers with PHEV vehicles
EV_SENSORS = [
SensorEntityDescription(
key=sc.EV_DISTANCE_TO_EMPTY,
device_class=SensorDeviceClass.DISTANCE,
icon="mdi:ev-station",
name="EV range",
native_unit_of_measurement=LENGTH_MILES,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=sc.EV_STATE_OF_CHARGE_PERCENT,
device_class=SensorDeviceClass.BATTERY,
name="EV battery level",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=sc.EV_TIME_TO_FULLY_CHARGED_UTC,
device_class=SensorDeviceClass.TIMESTAMP,
name="EV time to full charge",
state_class=SensorStateClass.MEASUREMENT,
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Subaru sensors by config_entry."""
entry = hass.data[DOMAIN][config_entry.entry_id]
coordinator = entry[ENTRY_COORDINATOR]
vehicle_info = entry[ENTRY_VEHICLES]
entities = []
await _async_migrate_entries(hass, config_entry)
for info in vehicle_info.values():
entities.extend(create_vehicle_sensors(info, coordinator))
async_add_entities(entities)
def create_vehicle_sensors(
vehicle_info, coordinator: DataUpdateCoordinator
) -> list[SubaruSensor]:
"""Instantiate all available sensors for the vehicle."""
sensor_descriptions_to_add = []
if vehicle_info[VEHICLE_HAS_SAFETY_SERVICE]:
sensor_descriptions_to_add.extend(SAFETY_SENSORS)
if vehicle_info[VEHICLE_API_GEN] == API_GEN_2:
sensor_descriptions_to_add.extend(API_GEN_2_SENSORS)
if vehicle_info[VEHICLE_HAS_EV]:
sensor_descriptions_to_add.extend(EV_SENSORS)
return [
SubaruSensor(
vehicle_info,
coordinator,
description,
)
for description in sensor_descriptions_to_add
]
class SubaruSensor(
CoordinatorEntity[DataUpdateCoordinator[dict[str, Any]]], SensorEntity
):
"""Class for Subaru sensors."""
_attr_has_entity_name = True
def __init__(
self,
vehicle_info: dict,
coordinator: DataUpdateCoordinator,
description: SensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.vin = vehicle_info[VEHICLE_VIN]
self.entity_description = description
self._attr_device_info = get_device_info(vehicle_info)
self._attr_unique_id = f"{self.vin}_{description.key}"
@property
def native_value(self) -> None | int | float:
"""Return the state of the sensor."""
vehicle_data = self.coordinator.data[self.vin]
current_value = vehicle_data[VEHICLE_STATUS].get(self.entity_description.key)
unit = self.entity_description.native_unit_of_measurement
unit_system = self.hass.config.units
if current_value is None:
return None
if unit in LENGTH_UNITS:
return round(unit_system.length(current_value, unit), 1)
if unit in PRESSURE_UNITS and unit_system == US_CUSTOMARY_SYSTEM:
return round(
unit_system.pressure(current_value, unit),
1,
)
if (
unit
in [
FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS,
FUEL_CONSUMPTION_MILES_PER_GALLON,
]
and unit_system == US_CUSTOMARY_SYSTEM
):
return round((100.0 * L_PER_GAL) / (KM_PER_MI * current_value), 1)
return current_value
@property
def native_unit_of_measurement(self) -> str | None:
"""Return the unit_of_measurement of the device."""
unit = self.entity_description.native_unit_of_measurement
if unit in LENGTH_UNITS:
return self.hass.config.units.length_unit
if unit in PRESSURE_UNITS:
if self.hass.config.units == US_CUSTOMARY_SYSTEM:
return self.hass.config.units.pressure_unit
if unit in [
FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS,
FUEL_CONSUMPTION_MILES_PER_GALLON,
]:
if self.hass.config.units == US_CUSTOMARY_SYSTEM:
return FUEL_CONSUMPTION_MILES_PER_GALLON
return unit
@property
def available(self) -> bool:
"""Return if entity is available."""
last_update_success = super().available
if last_update_success and self.vin not in self.coordinator.data:
return False
return last_update_success
async def _async_migrate_entries(
hass: HomeAssistant, config_entry: ConfigEntry
) -> None:
"""Migrate sensor entries from HA<=2022.10 to use preferred unique_id."""
entity_registry = er.async_get(hass)
all_sensors = []
all_sensors.extend(EV_SENSORS)
all_sensors.extend(API_GEN_2_SENSORS)
all_sensors.extend(SAFETY_SENSORS)
# Old unique_id is (previously title-cased) sensor name (e.g. "VIN_Avg Fuel Consumption")
replacements = {str(s.name).upper(): s.key for s in all_sensors}
@callback
def update_unique_id(entry: er.RegistryEntry) -> dict[str, Any] | None:
id_split = entry.unique_id.split("_")
key = id_split[1].upper() if len(id_split) == 2 else None
if key not in replacements or id_split[1] == replacements[key]:
return None
new_unique_id = entry.unique_id.replace(id_split[1], replacements[key])
_LOGGER.debug(
"Migrating entity '%s' unique_id from '%s' to '%s'",
entry.entity_id,
entry.unique_id,
new_unique_id,
)
if existing_entity_id := entity_registry.async_get_entity_id(
entry.domain, entry.platform, new_unique_id
):
_LOGGER.debug(
"Cannot migrate to unique_id '%s', already exists for '%s'",
new_unique_id,
existing_entity_id,
)
return None
return {
"new_unique_id": new_unique_id,
}
await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id)