Allow per-sensor unit conversion on BMW sensors (#110272)

* Update BMW sensors to use device_class

* Test adjustments

* Trigger CI

* Remove unneeded cast

* Set suggested_display_precision to 0

* Rebase for climate_status

* Change charging_status to ENUM device class

* Add test for Enum translations

* Pin Enum sensor values

* Use snapshot_platform helper

* Remove translation tests

* Formatting

* Remove comment

* Use const.STATE_UNKOWN

* Fix typo

* Update strings

* Loop through Enum sensors

* Revert enum sensor changes

---------

Co-authored-by: Richard <rikroe@users.noreply.github.com>
This commit is contained in:
Richard Kroegel 2024-06-04 09:06:23 +02:00 committed by GitHub
parent cba07540e9
commit 7fb2802910
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 172 additions and 76 deletions

View File

@ -6,9 +6,8 @@ from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
import datetime import datetime
import logging import logging
from typing import cast
from bimmer_connected.models import ValueWithUnit from bimmer_connected.models import StrEnum, ValueWithUnit
from bimmer_connected.vehicle import MyBMWVehicle from bimmer_connected.vehicle import MyBMWVehicle
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
@ -18,14 +17,19 @@ from homeassistant.components.sensor import (
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import LENGTH, PERCENTAGE, VOLUME, UnitOfElectricCurrent from homeassistant.const import (
PERCENTAGE,
STATE_UNKNOWN,
UnitOfElectricCurrent,
UnitOfLength,
UnitOfVolume,
)
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import BMWBaseEntity from . import BMWBaseEntity
from .const import CLIMATE_ACTIVITY_STATE, DOMAIN, UNIT_MAP from .const import CLIMATE_ACTIVITY_STATE, DOMAIN
from .coordinator import BMWDataUpdateCoordinator from .coordinator import BMWDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -36,34 +40,18 @@ class BMWSensorEntityDescription(SensorEntityDescription):
"""Describes BMW sensor entity.""" """Describes BMW sensor entity."""
key_class: str | None = None key_class: str | None = None
unit_type: str | None = None
value: Callable = lambda x, y: x
is_available: Callable[[MyBMWVehicle], bool] = lambda v: v.is_lsc_enabled is_available: Callable[[MyBMWVehicle], bool] = lambda v: v.is_lsc_enabled
def convert_and_round(
state: ValueWithUnit,
converter: Callable[[float | None, str], float],
precision: int,
) -> float | None:
"""Safely convert and round a value from ValueWithUnit."""
if state.value and state.unit:
return round(
converter(state.value, UNIT_MAP.get(state.unit, state.unit)), precision
)
if state.value:
return state.value
return None
SENSOR_TYPES: list[BMWSensorEntityDescription] = [ SENSOR_TYPES: list[BMWSensorEntityDescription] = [
# --- Generic ---
BMWSensorEntityDescription( BMWSensorEntityDescription(
key="ac_current_limit", key="ac_current_limit",
translation_key="ac_current_limit", translation_key="ac_current_limit",
key_class="charging_profile", key_class="charging_profile",
unit_type=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
suggested_display_precision=0,
is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
), ),
BMWSensorEntityDescription( BMWSensorEntityDescription(
@ -85,74 +73,81 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [
key="charging_status", key="charging_status",
translation_key="charging_status", translation_key="charging_status",
key_class="fuel_and_battery", key_class="fuel_and_battery",
value=lambda x, y: x.value,
is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
), ),
BMWSensorEntityDescription( BMWSensorEntityDescription(
key="charging_target", key="charging_target",
translation_key="charging_target", translation_key="charging_target",
key_class="fuel_and_battery", key_class="fuel_and_battery",
unit_type=PERCENTAGE, device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=0,
is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
), ),
BMWSensorEntityDescription( BMWSensorEntityDescription(
key="remaining_battery_percent", key="remaining_battery_percent",
translation_key="remaining_battery_percent", translation_key="remaining_battery_percent",
key_class="fuel_and_battery", key_class="fuel_and_battery",
unit_type=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY, device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
), ),
# --- Specific ---
BMWSensorEntityDescription( BMWSensorEntityDescription(
key="mileage", key="mileage",
translation_key="mileage", translation_key="mileage",
unit_type=LENGTH, device_class=SensorDeviceClass.DISTANCE,
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), native_unit_of_measurement=UnitOfLength.KILOMETERS,
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=0,
), ),
BMWSensorEntityDescription( BMWSensorEntityDescription(
key="remaining_range_total", key="remaining_range_total",
translation_key="remaining_range_total", translation_key="remaining_range_total",
key_class="fuel_and_battery", key_class="fuel_and_battery",
unit_type=LENGTH, device_class=SensorDeviceClass.DISTANCE,
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), native_unit_of_measurement=UnitOfLength.KILOMETERS,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
), ),
BMWSensorEntityDescription( BMWSensorEntityDescription(
key="remaining_range_electric", key="remaining_range_electric",
translation_key="remaining_range_electric", translation_key="remaining_range_electric",
key_class="fuel_and_battery", key_class="fuel_and_battery",
unit_type=LENGTH, device_class=SensorDeviceClass.DISTANCE,
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), native_unit_of_measurement=UnitOfLength.KILOMETERS,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
), ),
BMWSensorEntityDescription( BMWSensorEntityDescription(
key="remaining_range_fuel", key="remaining_range_fuel",
translation_key="remaining_range_fuel", translation_key="remaining_range_fuel",
key_class="fuel_and_battery", key_class="fuel_and_battery",
unit_type=LENGTH, device_class=SensorDeviceClass.DISTANCE,
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), native_unit_of_measurement=UnitOfLength.KILOMETERS,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain, is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain,
), ),
BMWSensorEntityDescription( BMWSensorEntityDescription(
key="remaining_fuel", key="remaining_fuel",
translation_key="remaining_fuel", translation_key="remaining_fuel",
key_class="fuel_and_battery", key_class="fuel_and_battery",
unit_type=VOLUME, device_class=SensorDeviceClass.VOLUME,
value=lambda x, hass: convert_and_round(x, hass.config.units.volume, 2), native_unit_of_measurement=UnitOfVolume.LITERS,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain, is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain,
), ),
BMWSensorEntityDescription( BMWSensorEntityDescription(
key="remaining_fuel_percent", key="remaining_fuel_percent",
translation_key="remaining_fuel_percent", translation_key="remaining_fuel_percent",
key_class="fuel_and_battery", key_class="fuel_and_battery",
unit_type=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain, is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain,
), ),
BMWSensorEntityDescription( BMWSensorEntityDescription(
@ -161,7 +156,6 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [
key_class="climate", key_class="climate",
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
options=CLIMATE_ACTIVITY_STATE, options=CLIMATE_ACTIVITY_STATE,
value=lambda x, _: x.lower() if x != "UNKNOWN" else None,
is_available=lambda v: v.is_remote_climate_stop_enabled, is_available=lambda v: v.is_remote_climate_stop_enabled,
), ),
] ]
@ -201,13 +195,6 @@ class BMWSensor(BMWBaseEntity, SensorEntity):
self.entity_description = description self.entity_description = description
self._attr_unique_id = f"{vehicle.vin}-{description.key}" self._attr_unique_id = f"{vehicle.vin}-{description.key}"
# Set the correct unit of measurement based on the unit_type
if description.unit_type:
self._attr_native_unit_of_measurement = (
coordinator.hass.config.units.as_dict().get(description.unit_type)
or description.unit_type
)
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator.""" """Handle updated data from the coordinator."""
@ -225,8 +212,18 @@ class BMWSensor(BMWBaseEntity, SensorEntity):
# For datetime without tzinfo, we assume it to be the same timezone as the HA instance # For datetime without tzinfo, we assume it to be the same timezone as the HA instance
if isinstance(state, datetime.datetime) and state.tzinfo is None: if isinstance(state, datetime.datetime) and state.tzinfo is None:
state = state.replace(tzinfo=dt_util.get_default_time_zone()) state = state.replace(tzinfo=dt_util.get_default_time_zone())
# For enum types, we only want the value
elif isinstance(state, ValueWithUnit):
state = state.value
# Get lowercase values from StrEnum
elif isinstance(state, StrEnum):
state = state.value.lower()
if state == STATE_UNKNOWN:
state = None
self._attr_native_value = cast( # special handling for charging_status to avoid a breaking change
StateType, self.entity_description.value(state, self.hass) if self.entity_description.key == "charging_status" and state:
) state = state.upper()
self._attr_native_value = state
super()._handle_coordinator_update() super()._handle_coordinator_update()

View File

@ -20,8 +20,11 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
'original_icon': None, 'original_icon': None,
'original_name': 'AC current limit', 'original_name': 'AC current limit',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -36,6 +39,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'current',
'friendly_name': 'i3 (+ REX) AC current limit', 'friendly_name': 'i3 (+ REX) AC current limit',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>, 'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
}), }),
@ -211,8 +215,11 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Charging target', 'original_name': 'Charging target',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -227,6 +234,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'battery',
'friendly_name': 'i3 (+ REX) Charging target', 'friendly_name': 'i3 (+ REX) Charging target',
'unit_of_measurement': '%', 'unit_of_measurement': '%',
}), }),
@ -261,8 +269,11 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Mileage', 'original_name': 'Mileage',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -277,6 +288,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'distance',
'friendly_name': 'i3 (+ REX) Mileage', 'friendly_name': 'i3 (+ REX) Mileage',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>, 'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>, 'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>,
@ -312,6 +324,9 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>, 'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None, 'original_icon': None,
@ -364,8 +379,11 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.VOLUME: 'volume'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Remaining fuel', 'original_name': 'Remaining fuel',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -380,6 +398,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'volume',
'friendly_name': 'i3 (+ REX) Remaining fuel', 'friendly_name': 'i3 (+ REX) Remaining fuel',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfVolume.LITERS: 'L'>, 'unit_of_measurement': <UnitOfVolume.LITERS: 'L'>,
@ -415,6 +434,9 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': None,
'original_icon': None, 'original_icon': None,
@ -466,8 +488,11 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Remaining range electric', 'original_name': 'Remaining range electric',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -482,6 +507,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'distance',
'friendly_name': 'i3 (+ REX) Remaining range electric', 'friendly_name': 'i3 (+ REX) Remaining range electric',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>, 'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>,
@ -517,8 +543,11 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Remaining range fuel', 'original_name': 'Remaining range fuel',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -533,6 +562,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'distance',
'friendly_name': 'i3 (+ REX) Remaining range fuel', 'friendly_name': 'i3 (+ REX) Remaining range fuel',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>, 'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>,
@ -568,8 +598,11 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Remaining range total', 'original_name': 'Remaining range total',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -584,6 +617,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'distance',
'friendly_name': 'i3 (+ REX) Remaining range total', 'friendly_name': 'i3 (+ REX) Remaining range total',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>, 'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>,
@ -617,8 +651,11 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
'original_icon': None, 'original_icon': None,
'original_name': 'AC current limit', 'original_name': 'AC current limit',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -633,6 +670,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'current',
'friendly_name': 'i4 eDrive40 AC current limit', 'friendly_name': 'i4 eDrive40 AC current limit',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>, 'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
}), }),
@ -808,8 +846,11 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Charging target', 'original_name': 'Charging target',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -824,6 +865,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'battery',
'friendly_name': 'i4 eDrive40 Charging target', 'friendly_name': 'i4 eDrive40 Charging target',
'unit_of_measurement': '%', 'unit_of_measurement': '%',
}), }),
@ -919,8 +961,11 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Mileage', 'original_name': 'Mileage',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -935,6 +980,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'distance',
'friendly_name': 'i4 eDrive40 Mileage', 'friendly_name': 'i4 eDrive40 Mileage',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>, 'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>, 'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>,
@ -970,6 +1016,9 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>, 'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None, 'original_icon': None,
@ -1022,8 +1071,11 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Remaining range electric', 'original_name': 'Remaining range electric',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -1038,6 +1090,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'distance',
'friendly_name': 'i4 eDrive40 Remaining range electric', 'friendly_name': 'i4 eDrive40 Remaining range electric',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>, 'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>,
@ -1073,8 +1126,11 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Remaining range total', 'original_name': 'Remaining range total',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -1089,6 +1145,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'distance',
'friendly_name': 'i4 eDrive40 Remaining range total', 'friendly_name': 'i4 eDrive40 Remaining range total',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>, 'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>,
@ -1122,8 +1179,11 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
'original_icon': None, 'original_icon': None,
'original_name': 'AC current limit', 'original_name': 'AC current limit',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -1138,6 +1198,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'current',
'friendly_name': 'iX xDrive50 AC current limit', 'friendly_name': 'iX xDrive50 AC current limit',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>, 'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
}), }),
@ -1313,8 +1374,11 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Charging target', 'original_name': 'Charging target',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -1329,6 +1393,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'battery',
'friendly_name': 'iX xDrive50 Charging target', 'friendly_name': 'iX xDrive50 Charging target',
'unit_of_measurement': '%', 'unit_of_measurement': '%',
}), }),
@ -1424,8 +1489,11 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Mileage', 'original_name': 'Mileage',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -1440,6 +1508,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'distance',
'friendly_name': 'iX xDrive50 Mileage', 'friendly_name': 'iX xDrive50 Mileage',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>, 'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>, 'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>,
@ -1475,6 +1544,9 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>, 'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None, 'original_icon': None,
@ -1527,8 +1599,11 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Remaining range electric', 'original_name': 'Remaining range electric',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -1543,6 +1618,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'distance',
'friendly_name': 'iX xDrive50 Remaining range electric', 'friendly_name': 'iX xDrive50 Remaining range electric',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>, 'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>,
@ -1578,8 +1654,11 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Remaining range total', 'original_name': 'Remaining range total',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -1594,6 +1673,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'distance',
'friendly_name': 'iX xDrive50 Remaining range total', 'friendly_name': 'iX xDrive50 Remaining range total',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>, 'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>,
@ -1690,8 +1770,11 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Mileage', 'original_name': 'Mileage',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -1706,6 +1789,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'distance',
'friendly_name': 'M340i xDrive Mileage', 'friendly_name': 'M340i xDrive Mileage',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>, 'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>, 'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>,
@ -1741,8 +1825,11 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.VOLUME: 'volume'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Remaining fuel', 'original_name': 'Remaining fuel',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -1757,6 +1844,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'volume',
'friendly_name': 'M340i xDrive Remaining fuel', 'friendly_name': 'M340i xDrive Remaining fuel',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfVolume.LITERS: 'L'>, 'unit_of_measurement': <UnitOfVolume.LITERS: 'L'>,
@ -1792,6 +1880,9 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': None,
'original_icon': None, 'original_icon': None,
@ -1843,8 +1934,11 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Remaining range fuel', 'original_name': 'Remaining range fuel',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -1859,6 +1953,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'distance',
'friendly_name': 'M340i xDrive Remaining range fuel', 'friendly_name': 'M340i xDrive Remaining range fuel',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>, 'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>,
@ -1894,8 +1989,11 @@
}), }),
'name': None, 'name': None,
'options': dict({ 'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Remaining range total', 'original_name': 'Remaining range total',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -1910,6 +2008,7 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'distance',
'friendly_name': 'M340i xDrive Remaining range total', 'friendly_name': 'M340i xDrive Remaining range total',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>, 'unit_of_measurement': <UnitOfLength.KILOMETERS: 'km'>,

View File

@ -43,17 +43,17 @@ async def test_entity_state_attrs(
("entity_id", "unit_system", "value", "unit_of_measurement"), ("entity_id", "unit_system", "value", "unit_of_measurement"),
[ [
("sensor.i3_rex_remaining_range_total", METRIC, "279", "km"), ("sensor.i3_rex_remaining_range_total", METRIC, "279", "km"),
("sensor.i3_rex_remaining_range_total", IMPERIAL, "173.36", "mi"), ("sensor.i3_rex_remaining_range_total", IMPERIAL, "173.362562634216", "mi"),
("sensor.i3_rex_mileage", METRIC, "137009", "km"), ("sensor.i3_rex_mileage", METRIC, "137009", "km"),
("sensor.i3_rex_mileage", IMPERIAL, "85133.45", "mi"), ("sensor.i3_rex_mileage", IMPERIAL, "85133.4456772449", "mi"),
("sensor.i3_rex_remaining_battery_percent", METRIC, "82", "%"), ("sensor.i3_rex_remaining_battery_percent", METRIC, "82", "%"),
("sensor.i3_rex_remaining_battery_percent", IMPERIAL, "82", "%"), ("sensor.i3_rex_remaining_battery_percent", IMPERIAL, "82", "%"),
("sensor.i3_rex_remaining_range_electric", METRIC, "174", "km"), ("sensor.i3_rex_remaining_range_electric", METRIC, "174", "km"),
("sensor.i3_rex_remaining_range_electric", IMPERIAL, "108.12", "mi"), ("sensor.i3_rex_remaining_range_electric", IMPERIAL, "108.118587449296", "mi"),
("sensor.i3_rex_remaining_fuel", METRIC, "6", "L"), ("sensor.i3_rex_remaining_fuel", METRIC, "6", "L"),
("sensor.i3_rex_remaining_fuel", IMPERIAL, "1.59", "gal"), ("sensor.i3_rex_remaining_fuel", IMPERIAL, "1.58503231414889", "gal"),
("sensor.i3_rex_remaining_range_fuel", METRIC, "105", "km"), ("sensor.i3_rex_remaining_range_fuel", METRIC, "105", "km"),
("sensor.i3_rex_remaining_range_fuel", IMPERIAL, "65.24", "mi"), ("sensor.i3_rex_remaining_range_fuel", IMPERIAL, "65.2439751849201", "mi"),
("sensor.m340i_xdrive_remaining_fuel_percent", METRIC, "80", "%"), ("sensor.m340i_xdrive_remaining_fuel_percent", METRIC, "80", "%"),
("sensor.m340i_xdrive_remaining_fuel_percent", IMPERIAL, "80", "%"), ("sensor.m340i_xdrive_remaining_fuel_percent", IMPERIAL, "80", "%"),
], ],