Adjust BMW enum sensors translations (#118754)

Co-authored-by: Richard <rikroe@users.noreply.github.com>
pull/119147/head
Richard Kroegel 2024-06-08 17:08:37 +02:00 committed by GitHub
parent a2504dafbc
commit 43343ea44a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 171 additions and 21 deletions

View File

@ -28,10 +28,3 @@ SCAN_INTERVALS = {
"north_america": 600, "north_america": 600,
"rest_of_world": 300, "rest_of_world": 300,
} }
CLIMATE_ACTIVITY_STATE: list[str] = [
"cooling",
"heating",
"inactive",
"standby",
]

View File

@ -33,8 +33,8 @@ class BMWSelectEntityDescription(SelectEntityDescription):
dynamic_options: Callable[[MyBMWVehicle], list[str]] | None = None dynamic_options: Callable[[MyBMWVehicle], list[str]] | None = None
SELECT_TYPES: dict[str, BMWSelectEntityDescription] = { SELECT_TYPES: tuple[BMWSelectEntityDescription, ...] = (
"ac_limit": BMWSelectEntityDescription( BMWSelectEntityDescription(
key="ac_limit", key="ac_limit",
translation_key="ac_limit", translation_key="ac_limit",
is_available=lambda v: v.is_remote_set_ac_limit_enabled, 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, unit_of_measurement=UnitOfElectricCurrent.AMPERE,
), ),
"charging_mode": BMWSelectEntityDescription( BMWSelectEntityDescription(
key="charging_mode", key="charging_mode",
translation_key="charging_mode", translation_key="charging_mode",
is_available=lambda v: v.is_charging_plan_supported, is_available=lambda v: v.is_charging_plan_supported,
options=[c.value.lower() for c in ChargingMode if c != ChargingMode.UNKNOWN], 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( remote_service=lambda v, o: v.remote_services.trigger_charging_profile_update(
charging_mode=ChargingMode(o) charging_mode=ChargingMode(o)
), ),
), ),
} )
async def async_setup_entry( async def async_setup_entry(
@ -76,7 +76,7 @@ async def async_setup_entry(
entities.extend( entities.extend(
[ [
BMWSelect(coordinator, vehicle, description) BMWSelect(coordinator, vehicle, description)
for description in SELECT_TYPES.values() for description in SELECT_TYPES
if description.is_available(vehicle) if description.is_available(vehicle)
] ]
) )

View File

@ -9,6 +9,8 @@ import logging
from bimmer_connected.models import StrEnum, ValueWithUnit from bimmer_connected.models import StrEnum, ValueWithUnit
from bimmer_connected.vehicle import MyBMWVehicle 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 ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
@ -29,7 +31,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
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 from .const import DOMAIN
from .coordinator import BMWDataUpdateCoordinator from .coordinator import BMWDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -73,6 +75,8 @@ 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",
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, is_available=lambda v: v.is_lsc_enabled and v.has_electric_drivetrain,
), ),
BMWSensorEntityDescription( BMWSensorEntityDescription(
@ -155,7 +159,11 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [
translation_key="climate_status", translation_key="climate_status",
key_class="climate", key_class="climate",
device_class=SensorDeviceClass.ENUM, 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, is_available=lambda v: v.is_remote_climate_stop_enabled,
), ),
] ]

View File

@ -152,7 +152,22 @@
'aliases': set({ 'aliases': set({
}), }),
'area_id': None, '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': <ANY>, 'config_entry_id': <ANY>,
'device_class': None, 'device_class': None,
'device_id': <ANY>, 'device_id': <ANY>,
@ -169,7 +184,7 @@
'name': None, 'name': None,
'options': dict({ 'options': dict({
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Charging status', 'original_name': 'Charging status',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -184,7 +199,22 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'enum',
'friendly_name': 'i3 (+ REX) Charging status', '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': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.i3_rex_charging_status', 'entity_id': 'sensor.i3_rex_charging_status',
@ -783,7 +813,22 @@
'aliases': set({ 'aliases': set({
}), }),
'area_id': None, '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': <ANY>, 'config_entry_id': <ANY>,
'device_class': None, 'device_class': None,
'device_id': <ANY>, 'device_id': <ANY>,
@ -800,7 +845,7 @@
'name': None, 'name': None,
'options': dict({ 'options': dict({
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Charging status', 'original_name': 'Charging status',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -815,7 +860,22 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'enum',
'friendly_name': 'i4 eDrive40 Charging status', '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': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.i4_edrive40_charging_status', 'entity_id': 'sensor.i4_edrive40_charging_status',
@ -1311,7 +1371,22 @@
'aliases': set({ 'aliases': set({
}), }),
'area_id': None, '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': <ANY>, 'config_entry_id': <ANY>,
'device_class': None, 'device_class': None,
'device_id': <ANY>, 'device_id': <ANY>,
@ -1328,7 +1403,7 @@
'name': None, 'name': None,
'options': dict({ 'options': dict({
}), }),
'original_device_class': None, 'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None, 'original_icon': None,
'original_name': 'Charging status', 'original_name': 'Charging status',
'platform': 'bmw_connected_drive', 'platform': 'bmw_connected_drive',
@ -1343,7 +1418,22 @@
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW', 'attribution': 'Data provided by MyBMW',
'device_class': 'enum',
'friendly_name': 'iX xDrive50 Charging status', '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': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.ix_xdrive50_charging_status', 'entity_id': 'sensor.ix_xdrive50_charging_status',

View File

@ -8,10 +8,13 @@ import pytest
import respx import respx
from syrupy.assertion import SnapshotAssertion 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.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import entity_registry as er 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 from . import check_remote_service_call, setup_mocked_integration
@ -152,3 +155,29 @@ async def test_service_call_fail(
target={"entity_id": entity_id}, target={"entity_id": entity_id},
) )
assert hass.states.get(entity_id).state == old_value 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

View File

@ -5,9 +5,13 @@ from unittest.mock import patch
import pytest import pytest
from syrupy.assertion import SnapshotAssertion 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.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.translation import async_get_translations
from homeassistant.util.unit_system import ( from homeassistant.util.unit_system import (
METRIC_SYSTEM as METRIC, METRIC_SYSTEM as METRIC,
US_CUSTOMARY_SYSTEM as IMPERIAL, US_CUSTOMARY_SYSTEM as IMPERIAL,
@ -77,3 +81,29 @@ async def test_unit_conversion(
entity = hass.states.get(entity_id) entity = hass.states.get(entity_id)
assert entity.state == value assert entity.state == value
assert entity.attributes.get("unit_of_measurement") == unit_of_measurement 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