From 43343ea44aa2925b6cffa4ab2ca8d963645a1f6a Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Sat, 8 Jun 2024 17:08:37 +0200 Subject: [PATCH] Adjust BMW enum sensors translations (#118754) Co-authored-by: Richard --- .../components/bmw_connected_drive/const.py | 7 -- .../components/bmw_connected_drive/select.py | 12 +-- .../components/bmw_connected_drive/sensor.py | 12 ++- .../snapshots/test_sensor.ambr | 102 ++++++++++++++++-- .../bmw_connected_drive/test_select.py | 29 +++++ .../bmw_connected_drive/test_sensor.py | 30 ++++++ 6 files changed, 171 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/const.py b/homeassistant/components/bmw_connected_drive/const.py index 5374b52e684..49990977f71 100644 --- a/homeassistant/components/bmw_connected_drive/const.py +++ b/homeassistant/components/bmw_connected_drive/const.py @@ -28,10 +28,3 @@ SCAN_INTERVALS = { "north_america": 600, "rest_of_world": 300, } - -CLIMATE_ACTIVITY_STATE: list[str] = [ - "cooling", - "heating", - "inactive", - "standby", -] diff --git a/homeassistant/components/bmw_connected_drive/select.py b/homeassistant/components/bmw_connected_drive/select.py index db54627b5b6..2522c6bf2a6 100644 --- a/homeassistant/components/bmw_connected_drive/select.py +++ b/homeassistant/components/bmw_connected_drive/select.py @@ -33,8 +33,8 @@ class BMWSelectEntityDescription(SelectEntityDescription): dynamic_options: Callable[[MyBMWVehicle], list[str]] | None = None -SELECT_TYPES: dict[str, BMWSelectEntityDescription] = { - "ac_limit": BMWSelectEntityDescription( +SELECT_TYPES: tuple[BMWSelectEntityDescription, ...] = ( + BMWSelectEntityDescription( key="ac_limit", translation_key="ac_limit", is_available=lambda v: v.is_remote_set_ac_limit_enabled, @@ -48,17 +48,17 @@ SELECT_TYPES: dict[str, BMWSelectEntityDescription] = { ), unit_of_measurement=UnitOfElectricCurrent.AMPERE, ), - "charging_mode": BMWSelectEntityDescription( + BMWSelectEntityDescription( key="charging_mode", translation_key="charging_mode", is_available=lambda v: v.is_charging_plan_supported, options=[c.value.lower() for c in ChargingMode if c != ChargingMode.UNKNOWN], - current_option=lambda v: str(v.charging_profile.charging_mode.value).lower(), # type: ignore[union-attr] + current_option=lambda v: v.charging_profile.charging_mode.value.lower(), # type: ignore[union-attr] remote_service=lambda v, o: v.remote_services.trigger_charging_profile_update( charging_mode=ChargingMode(o) ), ), -} +) async def async_setup_entry( @@ -76,7 +76,7 @@ async def async_setup_entry( entities.extend( [ BMWSelect(coordinator, vehicle, description) - for description in SELECT_TYPES.values() + for description in SELECT_TYPES if description.is_available(vehicle) ] ) diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 34169817f47..1d9737c7d5f 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -9,6 +9,8 @@ import logging from bimmer_connected.models import StrEnum, ValueWithUnit from bimmer_connected.vehicle import MyBMWVehicle +from bimmer_connected.vehicle.climate import ClimateActivityState +from bimmer_connected.vehicle.fuel_and_battery import ChargingState from homeassistant.components.sensor import ( SensorDeviceClass, @@ -29,7 +31,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util from . import BMWBaseEntity -from .const import CLIMATE_ACTIVITY_STATE, DOMAIN +from .const import DOMAIN from .coordinator import BMWDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -73,6 +75,8 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [ key="charging_status", translation_key="charging_status", key_class="fuel_and_battery", + device_class=SensorDeviceClass.ENUM, + options=[s.value.lower() for s in ChargingState if s != ChargingState.UNKNOWN], is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain, ), BMWSensorEntityDescription( @@ -155,7 +159,11 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [ translation_key="climate_status", key_class="climate", device_class=SensorDeviceClass.ENUM, - options=CLIMATE_ACTIVITY_STATE, + options=[ + s.value.lower() + for s in ClimateActivityState + if s != ClimateActivityState.UNKNOWN + ], is_available=lambda v: v.is_remote_climate_stop_enabled, ), ] diff --git a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr index eaa33038baf..6ba87c029ee 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr @@ -152,7 +152,22 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'default', + 'charging', + 'error', + 'complete', + 'fully_charged', + 'finished_fully_charged', + 'finished_not_full', + 'invalid', + 'not_charging', + 'plugged_in', + 'waiting_for_charging', + 'target_reached', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -169,7 +184,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Charging status', 'platform': 'bmw_connected_drive', @@ -184,7 +199,22 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'enum', 'friendly_name': 'i3 (+ REX) Charging status', + 'options': list([ + 'default', + 'charging', + 'error', + 'complete', + 'fully_charged', + 'finished_fully_charged', + 'finished_not_full', + 'invalid', + 'not_charging', + 'plugged_in', + 'waiting_for_charging', + 'target_reached', + ]), }), 'context': , 'entity_id': 'sensor.i3_rex_charging_status', @@ -783,7 +813,22 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'default', + 'charging', + 'error', + 'complete', + 'fully_charged', + 'finished_fully_charged', + 'finished_not_full', + 'invalid', + 'not_charging', + 'plugged_in', + 'waiting_for_charging', + 'target_reached', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -800,7 +845,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Charging status', 'platform': 'bmw_connected_drive', @@ -815,7 +860,22 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'enum', 'friendly_name': 'i4 eDrive40 Charging status', + 'options': list([ + 'default', + 'charging', + 'error', + 'complete', + 'fully_charged', + 'finished_fully_charged', + 'finished_not_full', + 'invalid', + 'not_charging', + 'plugged_in', + 'waiting_for_charging', + 'target_reached', + ]), }), 'context': , 'entity_id': 'sensor.i4_edrive40_charging_status', @@ -1311,7 +1371,22 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'default', + 'charging', + 'error', + 'complete', + 'fully_charged', + 'finished_fully_charged', + 'finished_not_full', + 'invalid', + 'not_charging', + 'plugged_in', + 'waiting_for_charging', + 'target_reached', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -1328,7 +1403,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Charging status', 'platform': 'bmw_connected_drive', @@ -1343,7 +1418,22 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by MyBMW', + 'device_class': 'enum', 'friendly_name': 'iX xDrive50 Charging status', + 'options': list([ + 'default', + 'charging', + 'error', + 'complete', + 'fully_charged', + 'finished_fully_charged', + 'finished_not_full', + 'invalid', + 'not_charging', + 'plugged_in', + 'waiting_for_charging', + 'target_reached', + ]), }), 'context': , 'entity_id': 'sensor.ix_xdrive50_charging_status', diff --git a/tests/components/bmw_connected_drive/test_select.py b/tests/components/bmw_connected_drive/test_select.py index 55e19482ef6..a270f38ee01 100644 --- a/tests/components/bmw_connected_drive/test_select.py +++ b/tests/components/bmw_connected_drive/test_select.py @@ -8,10 +8,13 @@ import pytest import respx from syrupy.assertion import SnapshotAssertion +from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN +from homeassistant.components.bmw_connected_drive.select import SELECT_TYPES from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.translation import async_get_translations from . import check_remote_service_call, setup_mocked_integration @@ -152,3 +155,29 @@ async def test_service_call_fail( target={"entity_id": entity_id}, ) assert hass.states.get(entity_id).state == old_value + + +@pytest.mark.usefixtures("bmw_fixture") +async def test_entity_option_translations( + hass: HomeAssistant, +) -> None: + """Ensure all enum sensor values are translated.""" + + # Setup component to load translations + assert await setup_mocked_integration(hass) + + prefix = f"component.{BMW_DOMAIN}.entity.{Platform.SELECT.value}" + + translations = await async_get_translations(hass, "en", "entity", [BMW_DOMAIN]) + translation_states = { + k for k in translations if k.startswith(prefix) and ".state." in k + } + + sensor_options = { + f"{prefix}.{entity_description.translation_key}.state.{option}" + for entity_description in SELECT_TYPES + if entity_description.options + for option in entity_description.options + } + + assert sensor_options == translation_states diff --git a/tests/components/bmw_connected_drive/test_sensor.py b/tests/components/bmw_connected_drive/test_sensor.py index 2f83fa108e5..c89df2caa7a 100644 --- a/tests/components/bmw_connected_drive/test_sensor.py +++ b/tests/components/bmw_connected_drive/test_sensor.py @@ -5,9 +5,13 @@ from unittest.mock import patch import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN +from homeassistant.components.bmw_connected_drive.sensor import SENSOR_TYPES +from homeassistant.components.sensor.const import SensorDeviceClass from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.translation import async_get_translations from homeassistant.util.unit_system import ( METRIC_SYSTEM as METRIC, US_CUSTOMARY_SYSTEM as IMPERIAL, @@ -77,3 +81,29 @@ async def test_unit_conversion( entity = hass.states.get(entity_id) assert entity.state == value assert entity.attributes.get("unit_of_measurement") == unit_of_measurement + + +@pytest.mark.usefixtures("bmw_fixture") +async def test_entity_option_translations( + hass: HomeAssistant, +) -> None: + """Ensure all enum sensor values are translated.""" + + # Setup component to load translations + assert await setup_mocked_integration(hass) + + prefix = f"component.{BMW_DOMAIN}.entity.{Platform.SENSOR.value}" + + translations = await async_get_translations(hass, "en", "entity", [BMW_DOMAIN]) + translation_states = { + k for k in translations if k.startswith(prefix) and ".state." in k + } + + sensor_options = { + f"{prefix}.{entity_description.translation_key}.state.{option}" + for entity_description in SENSOR_TYPES + if entity_description.device_class == SensorDeviceClass.ENUM + for option in entity_description.options + } + + assert sensor_options == translation_states