From 16fc935c8798df465957071b639702ed5e7ac135 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Mon, 8 Apr 2024 09:26:50 +0200 Subject: [PATCH] Refactor BMW entity availability (#110294) Co-authored-by: Richard --- .../bmw_connected_drive/binary_sensor.py | 6 +- .../components/bmw_connected_drive/lock.py | 2 +- .../components/bmw_connected_drive/sensor.py | 55 ++-- .../snapshots/test_sensor.ambr | 274 +++++++++--------- 4 files changed, 174 insertions(+), 163 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 85a0cbf8812..d40d85e4cd4 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -116,6 +116,7 @@ class BMWBinarySensorEntityDescription(BinarySensorEntityDescription): value_fn: Callable[[MyBMWVehicle], bool] attr_fn: Callable[[MyBMWVehicle, UnitSystem], dict[str, Any]] | None = None + is_available: Callable[[MyBMWVehicle], bool] = lambda v: v.is_lsc_enabled SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( @@ -174,12 +175,14 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.BATTERY_CHARGING, # device class power: On means power detected, Off means no power value_fn=lambda v: v.fuel_and_battery.charging_status == ChargingState.CHARGING, + is_available=lambda v: v.has_electric_drivetrain, ), BMWBinarySensorEntityDescription( key="connection_status", translation_key="connection_status", device_class=BinarySensorDeviceClass.PLUG, value_fn=lambda v: v.fuel_and_battery.is_charger_connected, + is_available=lambda v: v.has_electric_drivetrain, ), BMWBinarySensorEntityDescription( key="is_pre_entry_climatization_enabled", @@ -187,6 +190,7 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( value_fn=lambda v: v.charging_profile.is_pre_entry_climatization_enabled if v.charging_profile else False, + is_available=lambda v: v.has_electric_drivetrain, ), ) @@ -203,7 +207,7 @@ async def async_setup_entry( BMWBinarySensor(coordinator, vehicle, description, hass.config.units) for vehicle in coordinator.account.vehicles for description in SENSOR_TYPES - if description.key in vehicle.available_attributes + if description.is_available(vehicle) ] async_add_entities(entities) diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index 9529c135280..bbfadcef9db 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -51,7 +51,7 @@ class BMWLock(BMWBaseEntity, LockEntity): super().__init__(coordinator, vehicle) self._attr_unique_id = f"{vehicle.vin}-lock" - self.door_lock_state_available = DOOR_LOCK_STATE in vehicle.available_attributes + self.door_lock_state_available = vehicle.is_lsc_enabled async def async_lock(self, **kwargs: Any) -> None: """Lock the car.""" diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 49842305af0..e1ed398cfec 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -36,6 +36,7 @@ class BMWSensorEntityDescription(SensorEntityDescription): 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 def convert_and_round( @@ -53,57 +54,63 @@ def convert_and_round( return None -SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { +SENSOR_TYPES: list[BMWSensorEntityDescription] = [ # --- Generic --- - "ac_current_limit": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="ac_current_limit", translation_key="ac_current_limit", key_class="charging_profile", unit_type=UnitOfElectricCurrent.AMPERE, entity_registry_enabled_default=False, + is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), - "charging_start_time": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="charging_start_time", translation_key="charging_start_time", key_class="fuel_and_battery", device_class=SensorDeviceClass.TIMESTAMP, entity_registry_enabled_default=False, + is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), - "charging_end_time": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="charging_end_time", translation_key="charging_end_time", key_class="fuel_and_battery", device_class=SensorDeviceClass.TIMESTAMP, + is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), - "charging_status": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="charging_status", translation_key="charging_status", key_class="fuel_and_battery", value=lambda x, y: x.value, + is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), - "charging_target": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="charging_target", translation_key="charging_target", key_class="fuel_and_battery", unit_type=PERCENTAGE, + is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), - "remaining_battery_percent": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="remaining_battery_percent", translation_key="remaining_battery_percent", key_class="fuel_and_battery", unit_type=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, + is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), # --- Specific --- - "mileage": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="mileage", translation_key="mileage", unit_type=LENGTH, value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), state_class=SensorStateClass.TOTAL_INCREASING, ), - "remaining_range_total": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="remaining_range_total", translation_key="remaining_range_total", key_class="fuel_and_battery", @@ -111,38 +118,42 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), state_class=SensorStateClass.MEASUREMENT, ), - "remaining_range_electric": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="remaining_range_electric", translation_key="remaining_range_electric", key_class="fuel_and_battery", unit_type=LENGTH, value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), state_class=SensorStateClass.MEASUREMENT, + is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), - "remaining_range_fuel": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="remaining_range_fuel", translation_key="remaining_range_fuel", key_class="fuel_and_battery", unit_type=LENGTH, value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), state_class=SensorStateClass.MEASUREMENT, + is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain, ), - "remaining_fuel": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="remaining_fuel", translation_key="remaining_fuel", key_class="fuel_and_battery", unit_type=VOLUME, value=lambda x, hass: convert_and_round(x, hass.config.units.volume, 2), state_class=SensorStateClass.MEASUREMENT, + is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain, ), - "remaining_fuel_percent": BMWSensorEntityDescription( + BMWSensorEntityDescription( key="remaining_fuel_percent", translation_key="remaining_fuel_percent", key_class="fuel_and_battery", unit_type=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + is_available=lambda v: v.is_lsc_enabled and v.has_combustion_drivetrain, ), -} +] async def async_setup_entry( @@ -153,16 +164,12 @@ async def async_setup_entry( """Set up the MyBMW sensors from config entry.""" coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - entities: list[BMWSensor] = [] - - for vehicle in coordinator.account.vehicles: - entities.extend( - [ - BMWSensor(coordinator, vehicle, description) - for attribute_name in vehicle.available_attributes - if (description := SENSOR_TYPES.get(attribute_name)) - ] - ) + entities = [ + BMWSensor(coordinator, vehicle, description) + for vehicle in coordinator.account.vehicles + for description in SENSOR_TYPES + if description.is_available(vehicle) + ] async_add_entities(entities) diff --git a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr index e28b4485af0..c9dd4e3ddb8 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr @@ -1,34 +1,6 @@ # serializer version: 1 # name: test_entity_state_attrs list([ - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'iX xDrive50 Remaining range total', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.ix_xdrive50_remaining_range_total', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '340', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'iX xDrive50 Mileage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.ix_xdrive50_mileage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1121', - }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', @@ -54,6 +26,19 @@ 'last_updated': , 'state': 'CHARGING', }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Charging target', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.ix_xdrive50_charging_target', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80', + }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', @@ -69,6 +54,34 @@ 'last_updated': , 'state': '70', }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Mileage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ix_xdrive50_mileage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1121', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'iX xDrive50 Remaining range total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.ix_xdrive50_remaining_range_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '340', + }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', @@ -83,47 +96,6 @@ 'last_updated': , 'state': '340', }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'iX xDrive50 Charging target', - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.ix_xdrive50_charging_target', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '80', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 Remaining range total', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.i4_edrive40_remaining_range_total', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '472', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 Mileage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.i4_edrive40_mileage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1121', - }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', @@ -149,6 +121,19 @@ 'last_updated': , 'state': 'NOT_CHARGING', }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i4 eDrive40 Charging target', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.i4_edrive40_charging_target', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80', + }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', @@ -164,6 +149,34 @@ 'last_updated': , 'state': '80', }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i4 eDrive40 Mileage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.i4_edrive40_mileage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1121', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i4 eDrive40 Remaining range total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.i4_edrive40_remaining_range_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '472', + }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', @@ -181,15 +194,16 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i4 eDrive40 Charging target', - 'unit_of_measurement': '%', + 'friendly_name': 'M340i xDrive Mileage', + 'state_class': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.i4_edrive40_charging_target', + 'entity_id': 'sensor.m340i_xdrive_mileage', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '80', + 'state': '1121', }), StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -208,16 +222,16 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'M340i xDrive Mileage', - 'state_class': , + 'friendly_name': 'M340i xDrive Remaining range fuel', + 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.m340i_xdrive_mileage', + 'entity_id': 'sensor.m340i_xdrive_remaining_range_fuel', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1121', + 'state': '629', }), StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -233,20 +247,6 @@ 'last_updated': , 'state': '40', }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'M340i xDrive Remaining range fuel', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.m340i_xdrive_remaining_range_fuel', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '629', - }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', @@ -261,34 +261,6 @@ 'last_updated': , 'state': '80', }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Remaining range total', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.i3_rex_remaining_range_total', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '279', - }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Mileage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.i3_rex_mileage', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '137009', - }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', @@ -314,6 +286,19 @@ 'last_updated': , 'state': 'WAITING_FOR_CHARGING', }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i3 (+ REX) Charging target', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.i3_rex_charging_target', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', @@ -329,6 +314,34 @@ 'last_updated': , 'state': '82', }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i3 (+ REX) Mileage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.i3_rex_mileage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '137009', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by MyBMW', + 'friendly_name': 'i3 (+ REX) Remaining range total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.i3_rex_remaining_range_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '279', + }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', @@ -346,15 +359,16 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Charging target', - 'unit_of_measurement': '%', + 'friendly_name': 'i3 (+ REX) Remaining range fuel', + 'state_class': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.i3_rex_charging_target', + 'entity_id': 'sensor.i3_rex_remaining_range_fuel', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '100', + 'state': '105', }), StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -370,20 +384,6 @@ 'last_updated': , 'state': '6', }), - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'attribution': 'Data provided by MyBMW', - 'friendly_name': 'i3 (+ REX) Remaining range fuel', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.i3_rex_remaining_range_fuel', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '105', - }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW',