Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for reading vehicle status from MyBMW portal."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from dataclasses import dataclass
7 import datetime
8 import logging
9 
10 from bimmer_connected.models import StrEnum, ValueWithUnit
11 from bimmer_connected.vehicle import MyBMWVehicle
12 from bimmer_connected.vehicle.climate import ClimateActivityState
13 from bimmer_connected.vehicle.fuel_and_battery import ChargingState
14 
16  SensorDeviceClass,
17  SensorEntity,
18  SensorEntityDescription,
19  SensorStateClass,
20 )
21 from homeassistant.const import (
22  PERCENTAGE,
23  STATE_UNKNOWN,
24  UnitOfElectricCurrent,
25  UnitOfLength,
26  UnitOfPressure,
27  UnitOfVolume,
28 )
29 from homeassistant.core import HomeAssistant, callback
30 from homeassistant.helpers.entity_platform import AddEntitiesCallback
31 from homeassistant.util import dt as dt_util
32 
33 from . import BMWConfigEntry
34 from .coordinator import BMWDataUpdateCoordinator
35 from .entity import BMWBaseEntity
36 
37 _LOGGER = logging.getLogger(__name__)
38 
39 
40 @dataclass(frozen=True)
42  """Describes BMW sensor entity."""
43 
44  key_class: str | None = None
45  is_available: Callable[[MyBMWVehicle], bool] = lambda v: v.is_lsc_enabled
46 
47 
48 TIRES = ["front_left", "front_right", "rear_left", "rear_right"]
49 
50 SENSOR_TYPES: list[BMWSensorEntityDescription] = [
52  key="charging_profile.ac_current_limit",
53  translation_key="ac_current_limit",
54  device_class=SensorDeviceClass.CURRENT,
55  native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
56  entity_registry_enabled_default=False,
57  suggested_display_precision=0,
58  is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
59  ),
61  key="fuel_and_battery.charging_start_time",
62  translation_key="charging_start_time",
63  device_class=SensorDeviceClass.TIMESTAMP,
64  entity_registry_enabled_default=False,
65  is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
66  ),
68  key="fuel_and_battery.charging_end_time",
69  translation_key="charging_end_time",
70  device_class=SensorDeviceClass.TIMESTAMP,
71  is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
72  ),
74  key="fuel_and_battery.charging_status",
75  translation_key="charging_status",
76  device_class=SensorDeviceClass.ENUM,
77  options=[s.value.lower() for s in ChargingState if s != ChargingState.UNKNOWN],
78  is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
79  ),
81  key="fuel_and_battery.charging_target",
82  translation_key="charging_target",
83  native_unit_of_measurement=PERCENTAGE,
84  suggested_display_precision=0,
85  is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
86  ),
88  key="fuel_and_battery.remaining_battery_percent",
89  translation_key="remaining_battery_percent",
90  device_class=SensorDeviceClass.BATTERY,
91  native_unit_of_measurement=PERCENTAGE,
92  state_class=SensorStateClass.MEASUREMENT,
93  suggested_display_precision=0,
94  is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
95  ),
97  key="mileage",
98  translation_key="mileage",
99  device_class=SensorDeviceClass.DISTANCE,
100  native_unit_of_measurement=UnitOfLength.KILOMETERS,
101  state_class=SensorStateClass.TOTAL_INCREASING,
102  suggested_display_precision=0,
103  ),
105  key="fuel_and_battery.remaining_range_total",
106  translation_key="remaining_range_total",
107  device_class=SensorDeviceClass.DISTANCE,
108  native_unit_of_measurement=UnitOfLength.KILOMETERS,
109  state_class=SensorStateClass.MEASUREMENT,
110  suggested_display_precision=0,
111  ),
113  key="fuel_and_battery.remaining_range_electric",
114  translation_key="remaining_range_electric",
115  device_class=SensorDeviceClass.DISTANCE,
116  native_unit_of_measurement=UnitOfLength.KILOMETERS,
117  state_class=SensorStateClass.MEASUREMENT,
118  suggested_display_precision=0,
119  is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
120  ),
122  key="fuel_and_battery.remaining_range_fuel",
123  translation_key="remaining_range_fuel",
124  device_class=SensorDeviceClass.DISTANCE,
125  native_unit_of_measurement=UnitOfLength.KILOMETERS,
126  state_class=SensorStateClass.MEASUREMENT,
127  suggested_display_precision=0,
128  is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain,
129  ),
131  key="fuel_and_battery.remaining_fuel",
132  translation_key="remaining_fuel",
133  device_class=SensorDeviceClass.VOLUME_STORAGE,
134  native_unit_of_measurement=UnitOfVolume.LITERS,
135  state_class=SensorStateClass.MEASUREMENT,
136  suggested_display_precision=0,
137  is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain,
138  ),
140  key="fuel_and_battery.remaining_fuel_percent",
141  translation_key="remaining_fuel_percent",
142  native_unit_of_measurement=PERCENTAGE,
143  state_class=SensorStateClass.MEASUREMENT,
144  suggested_display_precision=0,
145  is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain,
146  ),
148  key="climate.activity",
149  translation_key="climate_status",
150  device_class=SensorDeviceClass.ENUM,
151  options=[
152  s.value.lower()
153  for s in ClimateActivityState
154  if s != ClimateActivityState.UNKNOWN
155  ],
156  is_available=lambda v: v.is_remote_climate_stop_enabled,
157  ),
158  *[
160  key=f"tires.{tire}.current_pressure",
161  translation_key=f"{tire}_current_pressure",
162  device_class=SensorDeviceClass.PRESSURE,
163  native_unit_of_measurement=UnitOfPressure.KPA,
164  suggested_unit_of_measurement=UnitOfPressure.BAR,
165  state_class=SensorStateClass.MEASUREMENT,
166  suggested_display_precision=2,
167  is_available=lambda v: v.is_lsc_enabled and v.tires is not None,
168  )
169  for tire in TIRES
170  ],
171  *[
173  key=f"tires.{tire}.target_pressure",
174  translation_key=f"{tire}_target_pressure",
175  device_class=SensorDeviceClass.PRESSURE,
176  native_unit_of_measurement=UnitOfPressure.KPA,
177  suggested_unit_of_measurement=UnitOfPressure.BAR,
178  state_class=SensorStateClass.MEASUREMENT,
179  suggested_display_precision=2,
180  entity_registry_enabled_default=False,
181  is_available=lambda v: v.is_lsc_enabled and v.tires is not None,
182  )
183  for tire in TIRES
184  ],
185 ]
186 
187 
189  hass: HomeAssistant,
190  config_entry: BMWConfigEntry,
191  async_add_entities: AddEntitiesCallback,
192 ) -> None:
193  """Set up the MyBMW sensors from config entry."""
194  coordinator = config_entry.runtime_data.coordinator
195 
196  entities = [
197  BMWSensor(coordinator, vehicle, description)
198  for vehicle in coordinator.account.vehicles
199  for description in SENSOR_TYPES
200  if description.is_available(vehicle)
201  ]
202 
203  async_add_entities(entities)
204 
205 
207  """Representation of a BMW vehicle sensor."""
208 
209  entity_description: BMWSensorEntityDescription
210 
211  def __init__(
212  self,
213  coordinator: BMWDataUpdateCoordinator,
214  vehicle: MyBMWVehicle,
215  description: BMWSensorEntityDescription,
216  ) -> None:
217  """Initialize BMW vehicle sensor."""
218  super().__init__(coordinator, vehicle)
219  self.entity_descriptionentity_description = description
220  self._attr_unique_id_attr_unique_id = f"{vehicle.vin}-{description.key}"
221 
222  @callback
223  def _handle_coordinator_update(self) -> None:
224  """Handle updated data from the coordinator."""
225  _LOGGER.debug(
226  "Updating sensor '%s' of %s", self.entity_descriptionentity_description.key, self.vehiclevehicle.name
227  )
228 
229  key_path = self.entity_descriptionentity_description.key.split(".")
230  state = getattr(self.vehiclevehicle, key_path.pop(0))
231 
232  for key in key_path:
233  state = getattr(state, key)
234 
235  # For datetime without tzinfo, we assume it to be the same timezone as the HA instance
236  if isinstance(state, datetime.datetime) and state.tzinfo is None:
237  state = state.replace(tzinfo=dt_util.get_default_time_zone())
238  # For enum types, we only want the value
239  elif isinstance(state, ValueWithUnit):
240  state = state.value
241  # Get lowercase values from StrEnum
242  elif isinstance(state, StrEnum):
243  state = state.value.lower()
244  if state == STATE_UNKNOWN:
245  state = None
246 
247  self._attr_native_value_attr_native_value = state
None __init__(self, BMWDataUpdateCoordinator coordinator, MyBMWVehicle vehicle, BMWSensorEntityDescription description)
Definition: sensor.py:216
None async_setup_entry(HomeAssistant hass, BMWConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:192