From 301ef0f636c735e0b89afcbc1e47b0748c84b0b0 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Wed, 24 Nov 2021 23:18:45 +0100 Subject: [PATCH] Fix BMW ConnectedDrive, update to My BMW API (#59881) * Fix BMW ConnectedDrive, update to My BMW API * Use const device classes * Implement native_value via EntityDescription * Use device class const, reomve device_class from charging_status * Cleanup * Remove max_range_electric * Revert removing sensor name & unique_id * Add region china again, update bimmer_connected * Update to bimmer_connected==0.8.2 Co-authored-by: rikroe --- .../bmw_connected_drive/__init__.py | 4 +- .../bmw_connected_drive/binary_sensor.py | 48 +- .../components/bmw_connected_drive/const.py | 14 + .../bmw_connected_drive/device_tracker.py | 9 +- .../components/bmw_connected_drive/lock.py | 8 +- .../bmw_connected_drive/manifest.json | 2 +- .../components/bmw_connected_drive/notify.py | 7 +- .../components/bmw_connected_drive/sensor.py | 499 ++---------------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 10 files changed, 103 insertions(+), 492 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index c7592c5db34..78a41ec8548 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -347,9 +347,9 @@ class BMWConnectedDriveBaseEntity(Entity): } self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, vehicle.vin)}, - manufacturer=vehicle.attributes.get("brand"), + manufacturer=vehicle.brand.name, model=vehicle.name, - name=f'{vehicle.attributes.get("brand")} {vehicle.name}', + name=f"{vehicle.brand.name} {vehicle.name}", ) def update_callback(self) -> None: diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 7ed23f72389..37c0271a034 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -6,11 +6,18 @@ from dataclasses import dataclass import logging from typing import Any, cast -from bimmer_connected.state import ChargingState, LockState, VehicleState from bimmer_connected.vehicle import ConnectedDriveVehicle -from bimmer_connected.vehicle_status import ConditionBasedServiceReport +from bimmer_connected.vehicle_status import ( + ChargingState, + ConditionBasedServiceReport, + LockState, + VehicleStatus, +) from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_LOCK, DEVICE_CLASS_OPENING, DEVICE_CLASS_PLUG, DEVICE_CLASS_PROBLEM, @@ -18,7 +25,6 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import LENGTH_KILOMETERS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.unit_system import UnitSystem @@ -28,13 +34,13 @@ from . import ( BMWConnectedDriveAccount, BMWConnectedDriveBaseEntity, ) -from .const import CONF_ACCOUNT, DATA_ENTRIES +from .const import CONF_ACCOUNT, DATA_ENTRIES, UNIT_MAP _LOGGER = logging.getLogger(__name__) def _are_doors_closed( - vehicle_state: VehicleState, extra_attributes: dict[str, Any], *args: Any + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any ) -> bool: # device class opening: On means open, Off means closed _LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed) @@ -44,7 +50,7 @@ def _are_doors_closed( def _are_windows_closed( - vehicle_state: VehicleState, extra_attributes: dict[str, Any], *args: Any + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any ) -> bool: # device class opening: On means open, Off means closed for window in vehicle_state.windows: @@ -53,7 +59,7 @@ def _are_windows_closed( def _are_doors_locked( - vehicle_state: VehicleState, extra_attributes: dict[str, Any], *args: Any + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any ) -> bool: # device class lock: On means unlocked, Off means locked # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED @@ -63,7 +69,7 @@ def _are_doors_locked( def _are_parking_lights_on( - vehicle_state: VehicleState, extra_attributes: dict[str, Any], *args: Any + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any ) -> bool: # device class light: On means light detected, Off means no light extra_attributes["lights_parking"] = vehicle_state.parking_lights.value @@ -71,7 +77,7 @@ def _are_parking_lights_on( def _are_problems_detected( - vehicle_state: VehicleState, + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], unit_system: UnitSystem, ) -> bool: @@ -82,7 +88,7 @@ def _are_problems_detected( def _check_control_messages( - vehicle_state: VehicleState, extra_attributes: dict[str, Any], *args: Any + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any ) -> bool: # device class problem: On means problem detected, Off means no problem check_control_messages = vehicle_state.check_control_messages @@ -96,7 +102,7 @@ def _check_control_messages( def _is_vehicle_charging( - vehicle_state: VehicleState, extra_attributes: dict[str, Any], *args: Any + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any ) -> bool: # device class power: On means power detected, Off means no power extra_attributes["charging_status"] = vehicle_state.charging_status.value @@ -107,7 +113,7 @@ def _is_vehicle_charging( def _is_vehicle_plugged_in( - vehicle_state: VehicleState, extra_attributes: dict[str, Any], *args: Any + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any ) -> bool: # device class plug: On means device is plugged in, # Off means device is unplugged @@ -124,7 +130,12 @@ def _format_cbs_report( if report.due_date is not None: result[f"{service_type} date"] = report.due_date.strftime("%Y-%m-%d") if report.due_distance is not None: - distance = round(unit_system.length(report.due_distance, LENGTH_KILOMETERS)) + distance = round( + unit_system.length( + report.due_distance[0], + UNIT_MAP.get(report.due_distance[1], report.due_distance[1]), + ) + ) result[f"{service_type} distance"] = f"{distance} {unit_system.length_unit}" return result @@ -133,7 +144,7 @@ def _format_cbs_report( class BMWRequiredKeysMixin: """Mixin for required keys.""" - value_fn: Callable[[VehicleState, dict[str, Any], UnitSystem], bool] + value_fn: Callable[[VehicleStatus, dict[str, Any], UnitSystem], bool] @dataclass @@ -161,14 +172,14 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( BMWBinarySensorEntityDescription( key="door_lock_state", name="Door lock state", - device_class="lock", + device_class=DEVICE_CLASS_LOCK, icon="mdi:car-key", value_fn=_are_doors_locked, ), BMWBinarySensorEntityDescription( key="lights_parking", name="Parking lights", - device_class="light", + device_class=DEVICE_CLASS_LIGHT, icon="mdi:car-parking-lights", value_fn=_are_parking_lights_on, ), @@ -190,7 +201,7 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( BMWBinarySensorEntityDescription( key="charging_status", name="Charging status", - device_class="power", + device_class=DEVICE_CLASS_BATTERY_CHARGING, icon="mdi:ev-station", value_fn=_is_vehicle_charging, ), @@ -245,7 +256,8 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): def update(self) -> None: """Read new state data from the library.""" - vehicle_state = self._vehicle.state + _LOGGER.debug("Updating binary sensors of %s", self._vehicle.name) + vehicle_state = self._vehicle.status result = self._attrs.copy() self._attr_is_on = self.entity_description.value_fn( diff --git a/homeassistant/components/bmw_connected_drive/const.py b/homeassistant/components/bmw_connected_drive/const.py index 7af24496838..83609d239c1 100644 --- a/homeassistant/components/bmw_connected_drive/const.py +++ b/homeassistant/components/bmw_connected_drive/const.py @@ -1,4 +1,11 @@ """Const file for the BMW Connected Drive integration.""" +from homeassistant.const import ( + LENGTH_KILOMETERS, + LENGTH_MILES, + VOLUME_GALLONS, + VOLUME_LITERS, +) + ATTRIBUTION = "Data provided by BMW Connected Drive" CONF_ALLOWED_REGIONS = ["china", "north_america", "rest_of_world"] @@ -9,3 +16,10 @@ CONF_ACCOUNT = "account" DATA_HASS_CONFIG = "hass_config" DATA_ENTRIES = "entries" + +UNIT_MAP = { + "KILOMETERS": LENGTH_KILOMETERS, + "MILES": LENGTH_MILES, + "LITERS": VOLUME_LITERS, + "GALLONS": VOLUME_GALLONS, +} diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index 4d7b6094968..d17920fef0c 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -35,7 +35,7 @@ async def async_setup_entry( for vehicle in account.account.vehicles: entities.append(BMWDeviceTracker(account, vehicle)) - if not vehicle.state.is_vehicle_tracking_enabled: + if not vehicle.status.is_vehicle_tracking_enabled: _LOGGER.info( "Tracking is (currently) disabled for vehicle %s (%s), defaulting to unknown", vehicle.name, @@ -59,7 +59,7 @@ class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity): super().__init__(account, vehicle) self._attr_unique_id = vehicle.vin - self._location = pos if (pos := vehicle.state.gps_position) else None + self._location = pos if (pos := vehicle.status.gps_position) else None self._attr_name = vehicle.name @property @@ -79,9 +79,10 @@ class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity): def update(self) -> None: """Update state of the decvice tracker.""" + _LOGGER.debug("Updating device tracker of %s", self._vehicle.name) self._attr_extra_state_attributes = self._attrs self._location = ( - self._vehicle.state.gps_position - if self._vehicle.state.is_vehicle_tracking_enabled + self._vehicle.status.gps_position + if self._vehicle.status.is_vehicle_tracking_enabled else None ) diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index 62e42476812..71539019f82 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -2,8 +2,8 @@ import logging from typing import Any -from bimmer_connected.state import LockState from bimmer_connected.vehicle import ConnectedDriveVehicle +from bimmer_connected.vehicle_status import LockState from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry @@ -78,8 +78,10 @@ class BMWLock(BMWConnectedDriveBaseEntity, LockEntity): def update(self) -> None: """Update state of the lock.""" - _LOGGER.debug("%s: updating data for %s", self._vehicle.name, self._attribute) - vehicle_state = self._vehicle.state + _LOGGER.debug( + "Updating lock data for '%s' of %s", self._attribute, self._vehicle.name + ) + vehicle_state = self._vehicle.status if not self.door_lock_state_available: self._attr_is_locked = None else: diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 110f8295c8a..95ea2061fb4 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.7.21"], + "requirements": ["bimmer_connected==0.8.2"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/homeassistant/components/bmw_connected_drive/notify.py b/homeassistant/components/bmw_connected_drive/notify.py index 4f4645bf78a..2db25aaa592 100644 --- a/homeassistant/components/bmw_connected_drive/notify.py +++ b/homeassistant/components/bmw_connected_drive/notify.py @@ -9,8 +9,6 @@ from bimmer_connected.vehicle import ConnectedDriveVehicle from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, - ATTR_TITLE, - ATTR_TITLE_DEFAULT, BaseNotificationService, ) from homeassistant.const import ATTR_LATITUDE, ATTR_LOCATION, ATTR_LONGITUDE, ATTR_NAME @@ -63,7 +61,6 @@ class BMWNotificationService(BaseNotificationService): _LOGGER.debug("Sending message to %s", vehicle.name) # Extract params from data dict - title: str = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) data = kwargs.get(ATTR_DATA) # Check if message is a POI @@ -84,6 +81,4 @@ class BMWNotificationService(BaseNotificationService): vehicle.remote_services.trigger_send_poi(location_dict) else: - vehicle.remote_services.trigger_send_message( - {ATTR_TEXT: message, ATTR_SUBJECT: title} - ) + raise ValueError(f"'data.{ATTR_LOCATION}' is required.") diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 4f0dc7904fc..3eda7ccd5b0 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -1,34 +1,28 @@ """Support for reading vehicle status from BMW connected drive portal.""" from __future__ import annotations -from copy import copy +from collections.abc import Callable from dataclasses import dataclass import logging +from typing import cast -from bimmer_connected.const import SERVICE_ALL_TRIPS, SERVICE_LAST_TRIP, SERVICE_STATUS -from bimmer_connected.state import ChargingState from bimmer_connected.vehicle import ConnectedDriveVehicle from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, - DEVICE_CLASS_TIMESTAMP, - ENERGY_KILO_WATT_HOUR, - ENERGY_WATT_HOUR, + DEVICE_CLASS_BATTERY, LENGTH_KILOMETERS, LENGTH_MILES, - MASS_KILOGRAMS, PERCENTAGE, TIME_HOURS, - TIME_MINUTES, VOLUME_GALLONS, VOLUME_LITERS, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.icon import icon_for_battery_level -import homeassistant.util.dt as dt_util +from homeassistant.helpers.typing import StateType from homeassistant.util.unit_system import UnitSystem from . import ( @@ -36,7 +30,7 @@ from . import ( BMWConnectedDriveAccount, BMWConnectedDriveBaseEntity, ) -from .const import CONF_ACCOUNT, DATA_ENTRIES +from .const import CONF_ACCOUNT, DATA_ENTRIES, UNIT_MAP _LOGGER = logging.getLogger(__name__) @@ -47,6 +41,7 @@ class BMWSensorEntityDescription(SensorEntityDescription): unit_metric: str | None = None unit_imperial: str | None = None + value: Callable = lambda x, y: x SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { @@ -59,58 +54,14 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { ), "charging_status": BMWSensorEntityDescription( key="charging_status", - icon="mdi:battery-charging", + icon="mdi:ev-station", + value=lambda x, y: x.value, ), - # No icon as this is dealt with directly as a special case in icon() "charging_level_hv": BMWSensorEntityDescription( key="charging_level_hv", unit_metric=PERCENTAGE, unit_imperial=PERCENTAGE, - ), - # LastTrip attributes - "date_utc": BMWSensorEntityDescription( - key="date_utc", - device_class=DEVICE_CLASS_TIMESTAMP, - ), - "duration": BMWSensorEntityDescription( - key="duration", - icon="mdi:timer-outline", - unit_metric=TIME_MINUTES, - unit_imperial=TIME_MINUTES, - ), - "electric_distance_ratio": BMWSensorEntityDescription( - key="electric_distance_ratio", - icon="mdi:percent-outline", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, - entity_registry_enabled_default=False, - ), - # AllTrips attributes - "battery_size_max": BMWSensorEntityDescription( - key="battery_size_max", - icon="mdi:battery-charging-high", - unit_metric=ENERGY_WATT_HOUR, - unit_imperial=ENERGY_WATT_HOUR, - entity_registry_enabled_default=False, - ), - "reset_date_utc": BMWSensorEntityDescription( - key="reset_date_utc", - device_class=DEVICE_CLASS_TIMESTAMP, - entity_registry_enabled_default=False, - ), - "saved_co2": BMWSensorEntityDescription( - key="saved_co2", - icon="mdi:tree-outline", - unit_metric=MASS_KILOGRAMS, - unit_imperial=MASS_KILOGRAMS, - entity_registry_enabled_default=False, - ), - "saved_co2_green_energy": BMWSensorEntityDescription( - key="saved_co2_green_energy", - icon="mdi:tree-outline", - unit_metric=MASS_KILOGRAMS, - unit_imperial=MASS_KILOGRAMS, - entity_registry_enabled_default=False, + device_class=DEVICE_CLASS_BATTERY, ), # --- Specific --- "mileage": BMWSensorEntityDescription( @@ -118,254 +69,61 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { icon="mdi:speedometer", unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, + value=lambda x, hass: round( + hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])) + ), ), "remaining_range_total": BMWSensorEntityDescription( key="remaining_range_total", icon="mdi:map-marker-distance", unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, + value=lambda x, hass: round( + hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])) + ), ), "remaining_range_electric": BMWSensorEntityDescription( key="remaining_range_electric", icon="mdi:map-marker-distance", unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, + value=lambda x, hass: round( + hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])) + ), ), "remaining_range_fuel": BMWSensorEntityDescription( key="remaining_range_fuel", icon="mdi:map-marker-distance", unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, - ), - "max_range_electric": BMWSensorEntityDescription( - key="max_range_electric", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, + value=lambda x, hass: round( + hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])) + ), ), "remaining_fuel": BMWSensorEntityDescription( key="remaining_fuel", icon="mdi:gas-station", unit_metric=VOLUME_LITERS, unit_imperial=VOLUME_GALLONS, + value=lambda x, hass: round( + hass.config.units.volume(x[0], UNIT_MAP.get(x[1], x[1])) + ), ), - # LastTrip attributes - "average_combined_consumption": BMWSensorEntityDescription( - key="average_combined_consumption", - icon="mdi:flash", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - ), - "average_electric_consumption": BMWSensorEntityDescription( - key="average_electric_consumption", - icon="mdi:power-plug-outline", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - ), - "average_recuperation": BMWSensorEntityDescription( - key="average_recuperation", - icon="mdi:recycle-variant", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - ), - "electric_distance": BMWSensorEntityDescription( - key="electric_distance", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - ), - "saved_fuel": BMWSensorEntityDescription( - key="saved_fuel", - icon="mdi:fuel", - unit_metric=VOLUME_LITERS, - unit_imperial=VOLUME_GALLONS, - entity_registry_enabled_default=False, - ), - "total_distance": BMWSensorEntityDescription( - key="total_distance", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - ), - # AllTrips attributes - "average_combined_consumption_community_average": BMWSensorEntityDescription( - key="average_combined_consumption_community_average", - icon="mdi:flash", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - entity_registry_enabled_default=False, - ), - "average_combined_consumption_community_high": BMWSensorEntityDescription( - key="average_combined_consumption_community_high", - icon="mdi:flash", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - entity_registry_enabled_default=False, - ), - "average_combined_consumption_community_low": BMWSensorEntityDescription( - key="average_combined_consumption_community_low", - icon="mdi:flash", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - entity_registry_enabled_default=False, - ), - "average_combined_consumption_user_average": BMWSensorEntityDescription( - key="average_combined_consumption_user_average", - icon="mdi:flash", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - ), - "average_electric_consumption_community_average": BMWSensorEntityDescription( - key="average_electric_consumption_community_average", - icon="mdi:power-plug-outline", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - entity_registry_enabled_default=False, - ), - "average_electric_consumption_community_high": BMWSensorEntityDescription( - key="average_electric_consumption_community_high", - icon="mdi:power-plug-outline", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - entity_registry_enabled_default=False, - ), - "average_electric_consumption_community_low": BMWSensorEntityDescription( - key="average_electric_consumption_community_low", - icon="mdi:power-plug-outline", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - entity_registry_enabled_default=False, - ), - "average_electric_consumption_user_average": BMWSensorEntityDescription( - key="average_electric_consumption_user_average", - icon="mdi:power-plug-outline", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - ), - "average_recuperation_community_average": BMWSensorEntityDescription( - key="average_recuperation_community_average", - icon="mdi:recycle-variant", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - entity_registry_enabled_default=False, - ), - "average_recuperation_community_high": BMWSensorEntityDescription( - key="average_recuperation_community_high", - icon="mdi:recycle-variant", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - entity_registry_enabled_default=False, - ), - "average_recuperation_community_low": BMWSensorEntityDescription( - key="average_recuperation_community_low", - icon="mdi:recycle-variant", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - entity_registry_enabled_default=False, - ), - "average_recuperation_user_average": BMWSensorEntityDescription( - key="average_recuperation_user_average", - icon="mdi:recycle-variant", - unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - ), - "chargecycle_range_community_average": BMWSensorEntityDescription( - key="chargecycle_range_community_average", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - entity_registry_enabled_default=False, - ), - "chargecycle_range_community_high": BMWSensorEntityDescription( - key="chargecycle_range_community_high", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - entity_registry_enabled_default=False, - ), - "chargecycle_range_community_low": BMWSensorEntityDescription( - key="chargecycle_range_community_low", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - entity_registry_enabled_default=False, - ), - "chargecycle_range_user_average": BMWSensorEntityDescription( - key="chargecycle_range_user_average", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - ), - "chargecycle_range_user_current_charge_cycle": BMWSensorEntityDescription( - key="chargecycle_range_user_current_charge_cycle", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - ), - "chargecycle_range_user_high": BMWSensorEntityDescription( - key="chargecycle_range_user_high", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - ), - "total_electric_distance_community_average": BMWSensorEntityDescription( - key="total_electric_distance_community_average", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - entity_registry_enabled_default=False, - ), - "total_electric_distance_community_high": BMWSensorEntityDescription( - key="total_electric_distance_community_high", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - entity_registry_enabled_default=False, - ), - "total_electric_distance_community_low": BMWSensorEntityDescription( - key="total_electric_distance_community_low", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - entity_registry_enabled_default=False, - ), - "total_electric_distance_user_average": BMWSensorEntityDescription( - key="total_electric_distance_user_average", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - entity_registry_enabled_default=False, - ), - "total_electric_distance_user_total": BMWSensorEntityDescription( - key="total_electric_distance_user_total", - icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, - entity_registry_enabled_default=False, - ), - "total_saved_fuel": BMWSensorEntityDescription( - key="total_saved_fuel", - icon="mdi:fuel", - unit_metric=VOLUME_LITERS, - unit_imperial=VOLUME_GALLONS, - entity_registry_enabled_default=False, + "fuel_percent": BMWSensorEntityDescription( + key="fuel_percent", + icon="mdi:gas-station", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, ), } -DEFAULT_BMW_DESCRIPTION = BMWSensorEntityDescription( - key="", - entity_registry_enabled_default=True, -) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the BMW ConnectedDrive sensors from config entry.""" - # pylint: disable=too-many-nested-blocks unit_system = hass.config.units account: BMWConnectedDriveAccount = hass.data[BMW_DOMAIN][DATA_ENTRIES][ config_entry.entry_id @@ -373,118 +131,13 @@ async def async_setup_entry( entities: list[BMWConnectedDriveSensor] = [] for vehicle in account.account.vehicles: - for service in vehicle.available_state_services: - if service == SERVICE_STATUS: - entities.extend( - [ - BMWConnectedDriveSensor( - account, vehicle, description, unit_system - ) - for attribute_name in vehicle.drive_train_attributes - if attribute_name in vehicle.available_attributes - and (description := SENSOR_TYPES.get(attribute_name)) - ] - ) - if service == SERVICE_LAST_TRIP: - entities.extend( - [ - # mypy issues will be fixed in next release - # https://github.com/python/mypy/issues/9096 - BMWConnectedDriveSensor( - account, - vehicle, - description, # type: ignore[arg-type] - unit_system, - service, - ) - for attribute_name in vehicle.state.last_trip.available_attributes - if attribute_name != "date" - and (description := SENSOR_TYPES.get(attribute_name)) # type: ignore[no-redef] - ] - ) - if "date" in vehicle.state.last_trip.available_attributes: - entities.append( - BMWConnectedDriveSensor( - account, - vehicle, - SENSOR_TYPES["date_utc"], - unit_system, - service, - ) - ) - if service == SERVICE_ALL_TRIPS: - for attribute_name in vehicle.state.all_trips.available_attributes: - if attribute_name == "reset_date": - entities.append( - BMWConnectedDriveSensor( - account, - vehicle, - SENSOR_TYPES["reset_date_utc"], - unit_system, - service, - ) - ) - elif attribute_name in ( - "average_combined_consumption", - "average_electric_consumption", - "average_recuperation", - "chargecycle_range", - "total_electric_distance", - ): - entities.extend( - [ - BMWConnectedDriveSensor( - account, - vehicle, - SENSOR_TYPES[f"{attribute_name}_{attr}"], - unit_system, - service, - ) - for attr in ( - "community_average", - "community_high", - "community_low", - "user_average", - ) - ] - ) - if attribute_name == "chargecycle_range": - entities.extend( - BMWConnectedDriveSensor( - account, - vehicle, - SENSOR_TYPES[f"{attribute_name}_{attr}"], - unit_system, - service, - ) - for attr in ("user_current_charge_cycle", "user_high") - ) - elif attribute_name == "total_electric_distance": - entities.extend( - [ - BMWConnectedDriveSensor( - account, - vehicle, - SENSOR_TYPES[f"{attribute_name}_{attr}"], - unit_system, - service, - ) - for attr in ("user_total",) - ] - ) - else: - if (description := SENSOR_TYPES.get(attribute_name)) is None: - description = copy(DEFAULT_BMW_DESCRIPTION) - description.key = attribute_name - entities.append( - BMWConnectedDriveSensor( - account, - vehicle, - description, - unit_system, - service, - ) - ) + entities.extend( + [ + BMWConnectedDriveSensor(account, vehicle, description, unit_system) + for attribute_name in vehicle.available_attributes + if (description := SENSOR_TYPES.get(attribute_name)) + ] + ) async_add_entities(entities, True) @@ -500,87 +153,21 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): vehicle: ConnectedDriveVehicle, description: BMWSensorEntityDescription, unit_system: UnitSystem, - service: str | None = None, ) -> None: """Initialize BMW vehicle sensor.""" super().__init__(account, vehicle) self.entity_description = description - self._service = service - if service: - self._attr_name = f"{vehicle.name} {service.lower()}_{description.key}" - self._attr_unique_id = f"{vehicle.vin}-{service.lower()}-{description.key}" - else: - self._attr_name = f"{vehicle.name} {description.key}" - self._attr_unique_id = f"{vehicle.vin}-{description.key}" + self._attr_name = f"{vehicle.name} {description.key}" + self._attr_unique_id = f"{vehicle.vin}-{description.key}" if unit_system.name == CONF_UNIT_SYSTEM_IMPERIAL: self._attr_native_unit_of_measurement = description.unit_imperial else: self._attr_native_unit_of_measurement = description.unit_metric - def update(self) -> None: - """Read new state data from the library.""" - _LOGGER.debug("Updating %s", self._vehicle.name) - vehicle_state = self._vehicle.state - sensor_key = self.entity_description.key - if sensor_key == "charging_status": - self._attr_native_value = getattr(vehicle_state, sensor_key).value - elif self.unit_of_measurement == VOLUME_GALLONS: - value = getattr(vehicle_state, sensor_key) - value_converted = self.hass.config.units.volume(value, VOLUME_LITERS) - self._attr_native_value = round(value_converted) - elif self.unit_of_measurement == LENGTH_MILES: - value = getattr(vehicle_state, sensor_key) - value_converted = self.hass.config.units.length(value, LENGTH_KILOMETERS) - self._attr_native_value = round(value_converted) - elif self._service is None: - self._attr_native_value = getattr(vehicle_state, sensor_key) - elif self._service == SERVICE_LAST_TRIP: - vehicle_last_trip = self._vehicle.state.last_trip - if sensor_key == "date_utc": - date_str = getattr(vehicle_last_trip, "date") - if parsed_date := dt_util.parse_datetime(date_str): - self._attr_native_value = parsed_date.isoformat() - else: - _LOGGER.debug( - "Could not parse date string for 'date_utc' sensor: %s", - date_str, - ) - self._attr_native_value = None - else: - self._attr_native_value = getattr(vehicle_last_trip, sensor_key) - elif self._service == SERVICE_ALL_TRIPS: - vehicle_all_trips = self._vehicle.state.all_trips - for attribute in ( - "average_combined_consumption", - "average_electric_consumption", - "average_recuperation", - "chargecycle_range", - "total_electric_distance", - ): - if sensor_key.startswith(f"{attribute}_"): - attr = getattr(vehicle_all_trips, attribute) - sub_attr = sensor_key.replace(f"{attribute}_", "") - self._attr_native_value = getattr(attr, sub_attr) - return - if sensor_key == "reset_date_utc": - date_str = getattr(vehicle_all_trips, "reset_date") - if parsed_date := dt_util.parse_datetime(date_str): - self._attr_native_value = parsed_date.isoformat() - else: - _LOGGER.debug( - "Could not parse date string for 'reset_date_utc' sensor: %s", - date_str, - ) - self._attr_native_value = None - else: - self._attr_native_value = getattr(vehicle_all_trips, sensor_key) - - vehicle_state = self._vehicle.state - charging_state = vehicle_state.charging_status in {ChargingState.CHARGING} - - if sensor_key == "charging_level_hv": - self._attr_icon = icon_for_battery_level( - battery_level=vehicle_state.charging_level_hv, charging=charging_state - ) + @property + def native_value(self) -> StateType: + """Return the state.""" + state = getattr(self._vehicle.status, self.entity_description.key) + return cast(StateType, self.entity_description.value(state, self.hass)) diff --git a/requirements_all.txt b/requirements_all.txt index e113992ce2d..e0c9a429cc4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -387,7 +387,7 @@ beautifulsoup4==4.10.0 bellows==0.28.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.21 +bimmer_connected==0.8.2 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ae165ba050..c7a5a0145f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -257,7 +257,7 @@ base36==0.1.1 bellows==0.28.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.21 +bimmer_connected==0.8.2 # homeassistant.components.blebox blebox_uniapi==1.3.3