Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for Subaru sensors."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 
8 import subarulink.const as sc
9 
11  SensorDeviceClass,
12  SensorEntity,
13  SensorEntityDescription,
14  SensorStateClass,
15 )
16 from homeassistant.config_entries import ConfigEntry
17 from homeassistant.const import PERCENTAGE, UnitOfLength, UnitOfPressure, UnitOfVolume
18 from homeassistant.core import HomeAssistant, callback
19 from homeassistant.helpers import entity_registry as er
20 from homeassistant.helpers.entity_platform import AddEntitiesCallback
22  CoordinatorEntity,
23  DataUpdateCoordinator,
24 )
25 from homeassistant.util.unit_conversion import DistanceConverter, VolumeConverter
26 from homeassistant.util.unit_system import METRIC_SYSTEM
27 
28 from . import get_device_info
29 from .const import (
30  API_GEN_2,
31  API_GEN_3,
32  DOMAIN,
33  ENTRY_COORDINATOR,
34  ENTRY_VEHICLES,
35  VEHICLE_API_GEN,
36  VEHICLE_HAS_EV,
37  VEHICLE_STATUS,
38  VEHICLE_VIN,
39 )
40 
41 _LOGGER = logging.getLogger(__name__)
42 
43 
44 # Fuel consumption units
45 FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS = "L/100km"
46 FUEL_CONSUMPTION_MILES_PER_GALLON = "mi/gal"
47 
48 L_PER_GAL = VolumeConverter.convert(1, UnitOfVolume.GALLONS, UnitOfVolume.LITERS)
49 KM_PER_MI = DistanceConverter.convert(1, UnitOfLength.MILES, UnitOfLength.KILOMETERS)
50 
51 # Sensor available for Gen1 or Gen2 vehicles
52 SAFETY_SENSORS = [
54  key=sc.ODOMETER,
55  translation_key="odometer",
56  device_class=SensorDeviceClass.DISTANCE,
57  native_unit_of_measurement=UnitOfLength.MILES,
58  state_class=SensorStateClass.TOTAL_INCREASING,
59  ),
60 ]
61 
62 # Sensors available to subscribers with Gen2/Gen3 vehicles
63 API_GEN_2_SENSORS = [
65  key=sc.AVG_FUEL_CONSUMPTION,
66  translation_key="average_fuel_consumption",
67  native_unit_of_measurement=FUEL_CONSUMPTION_MILES_PER_GALLON,
68  state_class=SensorStateClass.MEASUREMENT,
69  ),
71  key=sc.DIST_TO_EMPTY,
72  translation_key="range",
73  device_class=SensorDeviceClass.DISTANCE,
74  native_unit_of_measurement=UnitOfLength.MILES,
75  state_class=SensorStateClass.MEASUREMENT,
76  ),
78  key=sc.TIRE_PRESSURE_FL,
79  translation_key="tire_pressure_front_left",
80  device_class=SensorDeviceClass.PRESSURE,
81  native_unit_of_measurement=UnitOfPressure.PSI,
82  state_class=SensorStateClass.MEASUREMENT,
83  ),
85  key=sc.TIRE_PRESSURE_FR,
86  translation_key="tire_pressure_front_right",
87  device_class=SensorDeviceClass.PRESSURE,
88  native_unit_of_measurement=UnitOfPressure.PSI,
89  state_class=SensorStateClass.MEASUREMENT,
90  ),
92  key=sc.TIRE_PRESSURE_RL,
93  translation_key="tire_pressure_rear_left",
94  device_class=SensorDeviceClass.PRESSURE,
95  native_unit_of_measurement=UnitOfPressure.PSI,
96  state_class=SensorStateClass.MEASUREMENT,
97  ),
99  key=sc.TIRE_PRESSURE_RR,
100  translation_key="tire_pressure_rear_right",
101  device_class=SensorDeviceClass.PRESSURE,
102  native_unit_of_measurement=UnitOfPressure.PSI,
103  state_class=SensorStateClass.MEASUREMENT,
104  ),
105 ]
106 
107 # Sensors available for Gen3 vehicles
108 API_GEN_3_SENSORS = [
110  key=sc.REMAINING_FUEL_PERCENT,
111  translation_key="fuel_level",
112  native_unit_of_measurement=PERCENTAGE,
113  state_class=SensorStateClass.MEASUREMENT,
114  ),
115 ]
116 
117 # Sensors available to subscribers with PHEV vehicles
118 EV_SENSORS = [
120  key=sc.EV_DISTANCE_TO_EMPTY,
121  translation_key="ev_range",
122  device_class=SensorDeviceClass.DISTANCE,
123  native_unit_of_measurement=UnitOfLength.MILES,
124  state_class=SensorStateClass.MEASUREMENT,
125  ),
127  key=sc.EV_STATE_OF_CHARGE_PERCENT,
128  translation_key="ev_battery_level",
129  device_class=SensorDeviceClass.BATTERY,
130  native_unit_of_measurement=PERCENTAGE,
131  state_class=SensorStateClass.MEASUREMENT,
132  ),
134  key=sc.EV_TIME_TO_FULLY_CHARGED_UTC,
135  translation_key="ev_time_to_full_charge",
136  device_class=SensorDeviceClass.TIMESTAMP,
137  ),
138 ]
139 
140 
142  hass: HomeAssistant,
143  config_entry: ConfigEntry,
144  async_add_entities: AddEntitiesCallback,
145 ) -> None:
146  """Set up the Subaru sensors by config_entry."""
147  entry = hass.data[DOMAIN][config_entry.entry_id]
148  coordinator = entry[ENTRY_COORDINATOR]
149  vehicle_info = entry[ENTRY_VEHICLES]
150  entities = []
151  await _async_migrate_entries(hass, config_entry)
152  for info in vehicle_info.values():
153  entities.extend(create_vehicle_sensors(info, coordinator))
154  async_add_entities(entities)
155 
156 
158  vehicle_info, coordinator: DataUpdateCoordinator
159 ) -> list[SubaruSensor]:
160  """Instantiate all available sensors for the vehicle."""
161  sensor_descriptions_to_add = []
162  sensor_descriptions_to_add.extend(SAFETY_SENSORS)
163 
164  if vehicle_info[VEHICLE_API_GEN] in [API_GEN_2, API_GEN_3]:
165  sensor_descriptions_to_add.extend(API_GEN_2_SENSORS)
166 
167  if vehicle_info[VEHICLE_API_GEN] == API_GEN_3:
168  sensor_descriptions_to_add.extend(API_GEN_3_SENSORS)
169 
170  if vehicle_info[VEHICLE_HAS_EV]:
171  sensor_descriptions_to_add.extend(EV_SENSORS)
172 
173  return [
174  SubaruSensor(
175  vehicle_info,
176  coordinator,
177  description,
178  )
179  for description in sensor_descriptions_to_add
180  ]
181 
182 
184  CoordinatorEntity[DataUpdateCoordinator[dict[str, Any]]], SensorEntity
185 ):
186  """Class for Subaru sensors."""
187 
188  _attr_has_entity_name = True
189 
190  def __init__(
191  self,
192  vehicle_info: dict,
193  coordinator: DataUpdateCoordinator,
194  description: SensorEntityDescription,
195  ) -> None:
196  """Initialize the sensor."""
197  super().__init__(coordinator)
198  self.vinvin = vehicle_info[VEHICLE_VIN]
199  self.entity_descriptionentity_description = description
200  self._attr_device_info_attr_device_info = get_device_info(vehicle_info)
201  self._attr_unique_id_attr_unique_id = f"{self.vin}_{description.key}"
202 
203  @property
204  def native_value(self) -> int | float | None:
205  """Return the state of the sensor."""
206  current_value = self.coordinator.data[self.vinvin][VEHICLE_STATUS].get(
207  self.entity_descriptionentity_description.key
208  )
209 
210  if (
211  self.entity_descriptionentity_description.key == sc.AVG_FUEL_CONSUMPTION
212  and self.hasshasshass.config.units == METRIC_SYSTEM
213  ):
214  return round((100.0 * L_PER_GAL) / (KM_PER_MI * current_value), 1)
215 
216  return current_value
217 
218  @property
219  def native_unit_of_measurement(self) -> str | None:
220  """Return the unit_of_measurement of the device."""
221  if (
222  self.entity_descriptionentity_description.key == sc.AVG_FUEL_CONSUMPTION
223  and self.hasshasshass.config.units == METRIC_SYSTEM
224  ):
225  return FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS
226  return self.entity_descriptionentity_description.native_unit_of_measurement
227 
228  @property
229  def available(self) -> bool:
230  """Return if entity is available."""
231  last_update_success = super().available
232  if last_update_success and self.vinvin not in self.coordinator.data:
233  return False
234  return last_update_success
235 
236 
238  hass: HomeAssistant, config_entry: ConfigEntry
239 ) -> None:
240  """Migrate sensor entries from HA<=2022.10 to use preferred unique_id."""
241  entity_registry = er.async_get(hass)
242 
243  replacements = {
244  "ODOMETER": sc.ODOMETER,
245  "AVG FUEL CONSUMPTION": sc.AVG_FUEL_CONSUMPTION,
246  "RANGE": sc.DIST_TO_EMPTY,
247  "TIRE PRESSURE FL": sc.TIRE_PRESSURE_FL,
248  "TIRE PRESSURE FR": sc.TIRE_PRESSURE_FR,
249  "TIRE PRESSURE RL": sc.TIRE_PRESSURE_RL,
250  "TIRE PRESSURE RR": sc.TIRE_PRESSURE_RR,
251  "FUEL LEVEL": sc.REMAINING_FUEL_PERCENT,
252  "EV RANGE": sc.EV_DISTANCE_TO_EMPTY,
253  "EV BATTERY LEVEL": sc.EV_STATE_OF_CHARGE_PERCENT,
254  "EV TIME TO FULL CHARGE": sc.EV_TIME_TO_FULLY_CHARGED_UTC,
255  }
256 
257  @callback
258  def update_unique_id(entry: er.RegistryEntry) -> dict[str, Any] | None:
259  id_split = entry.unique_id.split("_")
260  key = id_split[1].upper() if len(id_split) == 2 else None
261 
262  if key not in replacements or id_split[1] == replacements[key]:
263  return None
264 
265  new_unique_id = entry.unique_id.replace(id_split[1], replacements[key])
266  _LOGGER.debug(
267  "Migrating entity '%s' unique_id from '%s' to '%s'",
268  entry.entity_id,
269  entry.unique_id,
270  new_unique_id,
271  )
272  if existing_entity_id := entity_registry.async_get_entity_id(
273  entry.domain, entry.platform, new_unique_id
274  ):
275  _LOGGER.debug(
276  "Cannot migrate to unique_id '%s', already exists for '%s'",
277  new_unique_id,
278  existing_entity_id,
279  )
280  return None
281  return {
282  "new_unique_id": new_unique_id,
283  }
284 
285  await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
None __init__(self, dict vehicle_info, DataUpdateCoordinator coordinator, SensorEntityDescription description)
Definition: sensor.py:195
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
dict[str, str]|None update_unique_id(er.RegistryEntry entity_entry, str unique_id)
Definition: __init__.py:168
list[SubaruSensor] create_vehicle_sensors(vehicle_info, DataUpdateCoordinator coordinator)
Definition: sensor.py:159
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:145
None _async_migrate_entries(HomeAssistant hass, ConfigEntry config_entry)
Definition: sensor.py:239
def get_device_info(vehicle_info)
Definition: __init__.py:167