diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 38415d0006f..48d28e26f8a 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -1,19 +1,24 @@ """Support for reading vehicle status from BMW connected drive portal.""" import logging +from bimmer_connected.const import SERVICE_LAST_TRIP, SERVICE_STATUS from bimmer_connected.state import ChargingState from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, + DEVICE_CLASS_TIMESTAMP, + ENERGY_KILO_WATT_HOUR, LENGTH_KILOMETERS, LENGTH_MILES, PERCENTAGE, TIME_HOURS, + TIME_MINUTES, VOLUME_GALLONS, VOLUME_LITERS, ) from homeassistant.helpers.icon import icon_for_battery_level +import homeassistant.util.dt as dt_util from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity from .const import CONF_ACCOUNT, DATA_ENTRIES @@ -21,31 +26,89 @@ from .const import CONF_ACCOUNT, DATA_ENTRIES _LOGGER = logging.getLogger(__name__) ATTR_TO_HA_METRIC = { - "mileage": ["mdi:speedometer", LENGTH_KILOMETERS], - "remaining_range_total": ["mdi:map-marker-distance", LENGTH_KILOMETERS], - "remaining_range_electric": ["mdi:map-marker-distance", LENGTH_KILOMETERS], - "remaining_range_fuel": ["mdi:map-marker-distance", LENGTH_KILOMETERS], - "max_range_electric": ["mdi:map-marker-distance", LENGTH_KILOMETERS], - "remaining_fuel": ["mdi:gas-station", VOLUME_LITERS], - "charging_time_remaining": ["mdi:update", TIME_HOURS], - "charging_status": ["mdi:battery-charging", None], - # No icon as this is dealt with directly as a special case in icon() - "charging_level_hv": [None, PERCENTAGE], + # "": [, , , ], + "mileage": ["mdi:speedometer", None, LENGTH_KILOMETERS, True], + "remaining_range_total": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], + "remaining_range_electric": [ + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + True, + ], + "remaining_range_fuel": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], + "max_range_electric": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], + "remaining_fuel": ["mdi:gas-station", None, VOLUME_LITERS, True], + # LastTrip attributes + "average_combined_consumption": [ + "mdi:flash", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + True, + ], + "average_electric_consumption": [ + "mdi:power-plug-outline", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + True, + ], + "average_recuperation": [ + "mdi:recycle-variant", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + True, + ], + "electric_distance": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], + "saved_fuel": ["mdi:fuel", None, VOLUME_LITERS, False], + "total_distance": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], } ATTR_TO_HA_IMPERIAL = { - "mileage": ["mdi:speedometer", LENGTH_MILES], - "remaining_range_total": ["mdi:map-marker-distance", LENGTH_MILES], - "remaining_range_electric": ["mdi:map-marker-distance", LENGTH_MILES], - "remaining_range_fuel": ["mdi:map-marker-distance", LENGTH_MILES], - "max_range_electric": ["mdi:map-marker-distance", LENGTH_MILES], - "remaining_fuel": ["mdi:gas-station", VOLUME_GALLONS], - "charging_time_remaining": ["mdi:update", TIME_HOURS], - "charging_status": ["mdi:battery-charging", None], - # No icon as this is dealt with directly as a special case in icon() - "charging_level_hv": [None, PERCENTAGE], + # "": [, , , ], + "mileage": ["mdi:speedometer", None, LENGTH_MILES, True], + "remaining_range_total": ["mdi:map-marker-distance", None, LENGTH_MILES, True], + "remaining_range_electric": ["mdi:map-marker-distance", None, LENGTH_MILES, True], + "remaining_range_fuel": ["mdi:map-marker-distance", None, LENGTH_MILES, True], + "max_range_electric": ["mdi:map-marker-distance", None, LENGTH_MILES, True], + "remaining_fuel": ["mdi:gas-station", None, VOLUME_GALLONS, True], + # LastTrip attributes + "average_combined_consumption": [ + "mdi:flash", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + True, + ], + "average_electric_consumption": [ + "mdi:power-plug-outline", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + True, + ], + "average_recuperation": [ + "mdi:recycle-variant", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + True, + ], + "electric_distance": ["mdi:map-marker-distance", None, LENGTH_MILES, True], + "saved_fuel": ["mdi:fuel", None, VOLUME_GALLONS, False], + "total_distance": ["mdi:map-marker-distance", None, LENGTH_MILES, True], } +ATTR_TO_HA_GENERIC = { + # "": [, , , ], + "charging_time_remaining": ["mdi:update", None, TIME_HOURS, True], + "charging_status": ["mdi:battery-charging", None, None, True], + # No icon as this is dealt with directly as a special case in icon() + "charging_level_hv": [None, None, PERCENTAGE, True], + # LastTrip attributes + "date_utc": [None, DEVICE_CLASS_TIMESTAMP, None, True], + "duration": ["mdi:timer-outline", None, TIME_MINUTES, True], + "electric_distance_ratio": ["mdi:percent-outline", None, PERCENTAGE, False], +} + +ATTR_TO_HA_METRIC.update(ATTR_TO_HA_GENERIC) +ATTR_TO_HA_IMPERIAL.update(ATTR_TO_HA_GENERIC) + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the BMW ConnectedDrive sensors from config entry.""" @@ -58,26 +121,54 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] for vehicle in account.account.vehicles: - for attribute_name in vehicle.drive_train_attributes: - if attribute_name in vehicle.available_attributes: - device = BMWConnectedDriveSensor( - account, vehicle, attribute_name, attribute_info - ) - entities.append(device) + for service in vehicle.available_state_services: + if service == SERVICE_STATUS: + for attribute_name in vehicle.drive_train_attributes: + if attribute_name in vehicle.available_attributes: + device = BMWConnectedDriveSensor( + account, vehicle, attribute_name, attribute_info + ) + entities.append(device) + if service == SERVICE_LAST_TRIP: + for attribute_name in vehicle.state.last_trip.available_attributes: + if attribute_name == "date": + device = BMWConnectedDriveSensor( + account, + vehicle, + "date_utc", + attribute_info, + service, + ) + entities.append(device) + else: + device = BMWConnectedDriveSensor( + account, vehicle, attribute_name, attribute_info, service + ) + entities.append(device) + async_add_entities(entities, True) class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): """Representation of a BMW vehicle sensor.""" - def __init__(self, account, vehicle, attribute: str, attribute_info): + def __init__(self, account, vehicle, attribute: str, attribute_info, service=None): """Initialize BMW vehicle sensor.""" super().__init__(account, vehicle) self._attribute = attribute + self._service = service self._state = None - self._name = f"{self._vehicle.name} {self._attribute}" - self._unique_id = f"{self._vehicle.vin}-{self._attribute}" + if self._service: + self._name = ( + f"{self._vehicle.name} {self._service.lower()}_{self._attribute}" + ) + self._unique_id = ( + f"{self._vehicle.vin}-{self._service.lower()}-{self._attribute}" + ) + else: + self._name = f"{self._vehicle.name} {self._attribute}" + self._unique_id = f"{self._vehicle.vin}-{self._attribute}" self._attribute_info = attribute_info @property @@ -100,9 +191,17 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): return icon_for_battery_level( battery_level=vehicle_state.charging_level_hv, charging=charging_state ) - icon, _ = self._attribute_info.get(self._attribute, [None, None]) + icon = self._attribute_info.get(self._attribute, [None, None, None, None])[0] return icon + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + enabled_default = self._attribute_info.get( + self._attribute, [None, None, None, True] + )[3] + return enabled_default + @property def state(self): """Return the state of the sensor. @@ -112,16 +211,23 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): """ return self._state + @property + def device_class(self) -> str: + """Get the device class.""" + clss = self._attribute_info.get(self._attribute, [None, None, None, None])[1] + return clss + @property def unit_of_measurement(self) -> str: """Get the unit of measurement.""" - unit = self._attribute_info.get(self._attribute, [None, None])[1] + unit = self._attribute_info.get(self._attribute, [None, None, None, None])[2] return unit def update(self) -> None: """Read new state data from the library.""" _LOGGER.debug("Updating %s", self._vehicle.name) vehicle_state = self._vehicle.state + vehicle_last_trip = self._vehicle.state.last_trip if self._attribute == "charging_status": self._state = getattr(vehicle_state, self._attribute).value elif self.unit_of_measurement == VOLUME_GALLONS: @@ -132,5 +238,11 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): value = getattr(vehicle_state, self._attribute) value_converted = self.hass.config.units.length(value, LENGTH_KILOMETERS) self._state = round(value_converted) - else: + elif self._service is None: self._state = getattr(vehicle_state, self._attribute) + elif self._service == SERVICE_LAST_TRIP: + if self._attribute == "date_utc": + date_str = getattr(vehicle_last_trip, "date") + self._state = dt_util.parse_datetime(date_str).isoformat() + else: + self._state = getattr(vehicle_last_trip, self._attribute)