Improve BMW translations (#133236)

This commit is contained in:
Richard Kroegel 2024-12-15 12:16:10 +01:00 committed by GitHub
parent ebc8ca8419
commit 8953ac1357
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 209 additions and 64 deletions

View File

@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import BMWConfigEntry
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
from .entity import BMWBaseEntity
if TYPE_CHECKING:
@ -55,7 +55,6 @@ BUTTON_TYPES: tuple[BMWButtonEntityDescription, ...] = (
BMWButtonEntityDescription(
key="deactivate_air_conditioning",
translation_key="deactivate_air_conditioning",
name="Deactivate air conditioning",
remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_air_conditioning_stop(),
is_available=lambda vehicle: vehicle.is_remote_climate_stop_enabled,
),
@ -111,6 +110,10 @@ class BMWButton(BMWBaseEntity, ButtonEntity):
try:
await self.entity_description.remote_function(self.vehicle)
except MyBMWAPIError as ex:
raise HomeAssistantError(ex) from ex
raise HomeAssistantError(
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
self.coordinator.async_update_listeners()

View File

@ -22,7 +22,13 @@ from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.ssl import get_default_context
from .const import CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN, SCAN_INTERVALS
from .const import (
CONF_GCID,
CONF_READ_ONLY,
CONF_REFRESH_TOKEN,
DOMAIN as BMW_DOMAIN,
SCAN_INTERVALS,
)
_LOGGER = logging.getLogger(__name__)
@ -57,7 +63,7 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
hass,
_LOGGER,
config_entry=config_entry,
name=f"{DOMAIN}-{config_entry.data[CONF_USERNAME]}",
name=f"{BMW_DOMAIN}-{config_entry.data[CONF_USERNAME]}",
update_interval=timedelta(
seconds=SCAN_INTERVALS[config_entry.data[CONF_REGION]]
),
@ -75,18 +81,29 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
except MyBMWCaptchaMissingError as err:
# If a captcha is required (user/password login flow), always trigger the reauth flow
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="missing_captcha",
) from err
except MyBMWAuthError as err:
# Allow one retry interval before raising AuthFailed to avoid flaky API issues
if self.last_update_success:
raise UpdateFailed(err) from err
raise UpdateFailed(
translation_domain=BMW_DOMAIN,
translation_key="update_failed",
translation_placeholders={"exception": str(err)},
) from err
# Clear refresh token and trigger reauth if previous update failed as well
self._update_config_entry_refresh_token(None)
raise ConfigEntryAuthFailed(err) from err
raise ConfigEntryAuthFailed(
translation_domain=BMW_DOMAIN,
translation_key="invalid_auth",
) from err
except (MyBMWAPIError, RequestError) as err:
raise UpdateFailed(err) from err
raise UpdateFailed(
translation_domain=BMW_DOMAIN,
translation_key="update_failed",
translation_placeholders={"exception": str(err)},
) from err
if self.account.refresh_token != old_refresh_token:
self._update_config_entry_refresh_token(self.account.refresh_token)

View File

@ -49,7 +49,7 @@ class BMWDeviceTracker(BMWBaseEntity, TrackerEntity):
_attr_force_update = False
_attr_translation_key = "car"
_attr_icon = "mdi:car"
_attr_name = None
def __init__(
self,
@ -58,9 +58,7 @@ class BMWDeviceTracker(BMWBaseEntity, TrackerEntity):
) -> None:
"""Initialize the Tracker."""
super().__init__(coordinator, vehicle)
self._attr_unique_id = vehicle.vin
self._attr_name = None
@property
def extra_state_attributes(self) -> dict[str, Any]:

View File

@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import BMWConfigEntry
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
from .coordinator import BMWDataUpdateCoordinator
from .entity import BMWBaseEntity
@ -70,7 +70,11 @@ class BMWLock(BMWBaseEntity, LockEntity):
# Set the state to unknown if the command fails
self._attr_is_locked = None
self.async_write_ha_state()
raise HomeAssistantError(ex) from ex
raise HomeAssistantError(
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
finally:
# Always update the listeners to get the latest state
self.coordinator.async_update_listeners()
@ -90,7 +94,11 @@ class BMWLock(BMWBaseEntity, LockEntity):
# Set the state to unknown if the command fails
self._attr_is_locked = None
self.async_write_ha_state()
raise HomeAssistantError(ex) from ex
raise HomeAssistantError(
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
finally:
# Always update the listeners to get the latest state
self.coordinator.async_update_listeners()

View File

@ -20,7 +20,7 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN, BMWConfigEntry
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
PARALLEL_UPDATES = 1
@ -92,7 +92,7 @@ class BMWNotificationService(BaseNotificationService):
except (vol.Invalid, TypeError, ValueError) as ex:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="invalid_poi",
translation_placeholders={
"poi_exception": str(ex),
@ -106,4 +106,8 @@ class BMWNotificationService(BaseNotificationService):
try:
await vehicle.remote_services.trigger_send_poi(poi)
except MyBMWAPIError as ex:
raise HomeAssistantError(ex) from ex
raise HomeAssistantError(
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex

View File

@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import BMWConfigEntry
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
from .coordinator import BMWDataUpdateCoordinator
from .entity import BMWBaseEntity
@ -109,6 +109,10 @@ class BMWNumber(BMWBaseEntity, NumberEntity):
try:
await self.entity_description.remote_service(self.vehicle, value)
except MyBMWAPIError as ex:
raise HomeAssistantError(ex) from ex
raise HomeAssistantError(
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
self.coordinator.async_update_listeners()

View File

@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import BMWConfigEntry
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
from .coordinator import BMWDataUpdateCoordinator
from .entity import BMWBaseEntity
@ -123,6 +123,10 @@ class BMWSelect(BMWBaseEntity, SelectEntity):
try:
await self.entity_description.remote_service(self.vehicle, option)
except MyBMWAPIError as ex:
raise HomeAssistantError(ex) from ex
raise HomeAssistantError(
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
self.coordinator.async_update_listeners()

View File

@ -2,11 +2,16 @@
"config": {
"step": {
"user": {
"description": "Enter your MyBMW/MINI Connected credentials.",
"description": "Connect to your MyBMW/MINI Connected account to retrieve vehicle data.",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"region": "ConnectedDrive Region"
},
"data_description": {
"username": "The email address of your MyBMW/MINI Connected account.",
"password": "The password of your MyBMW/MINI Connected account.",
"region": "The region of your MyBMW/MINI Connected account."
}
},
"captcha": {
@ -23,6 +28,9 @@
"description": "Update your MyBMW/MINI Connected password for account `{username}` in region `{region}`.",
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"password": "[%key:component::bmw_connected_drive::config::step::user::data_description::password%]"
}
}
},
@ -41,7 +49,10 @@
"step": {
"account_options": {
"data": {
"read_only": "Read-only (only sensors and notify, no execution of services, no lock)"
"read_only": "Read-only mode"
},
"data_description": {
"read_only": "Only retrieve values and send POI data, but don't offer any services that can change the vehicle state."
}
}
}
@ -83,6 +94,9 @@
"activate_air_conditioning": {
"name": "Activate air conditioning"
},
"deactivate_air_conditioning": {
"name": "Deactivate air conditioning"
},
"find_vehicle": {
"name": "Find vehicle"
}
@ -220,6 +234,15 @@
},
"missing_captcha": {
"message": "Login requires captcha validation"
},
"invalid_auth": {
"message": "[%key:common::config_flow::error::invalid_auth%]"
},
"remote_service_error": {
"message": "Error executing remote service on vehicle. {exception}"
},
"update_failed": {
"message": "Error updating vehicle data. {exception}"
}
}
}

View File

@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import BMWConfigEntry
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
from .coordinator import BMWDataUpdateCoordinator
from .entity import BMWBaseEntity
@ -111,8 +111,11 @@ class BMWSwitch(BMWBaseEntity, SwitchEntity):
try:
await self.entity_description.remote_service_on(self.vehicle)
except MyBMWAPIError as ex:
raise HomeAssistantError(ex) from ex
raise HomeAssistantError(
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
self.coordinator.async_update_listeners()
async def async_turn_off(self, **kwargs: Any) -> None:
@ -120,6 +123,9 @@ class BMWSwitch(BMWBaseEntity, SwitchEntity):
try:
await self.entity_description.remote_service_off(self.vehicle)
except MyBMWAPIError as ex:
raise HomeAssistantError(ex) from ex
raise HomeAssistantError(
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
self.coordinator.async_update_listeners()

View File

@ -48,6 +48,11 @@ FIXTURE_CONFIG_ENTRY = {
"unique_id": f"{FIXTURE_USER_INPUT[CONF_REGION]}-{FIXTURE_USER_INPUT[CONF_USERNAME]}",
}
REMOTE_SERVICE_EXC_REASON = "HTTPStatusError: 502 Bad Gateway"
REMOTE_SERVICE_EXC_TRANSLATION = (
"Error executing remote service on vehicle. HTTPStatusError: 502 Bad Gateway"
)
async def setup_mocked_integration(hass: HomeAssistant) -> MockConfigEntry:
"""Mock a fully setup config entry and all components based on fixtures."""

View File

@ -13,7 +13,11 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import check_remote_service_call, setup_mocked_integration
from . import (
REMOTE_SERVICE_EXC_TRANSLATION,
check_remote_service_call,
setup_mocked_integration,
)
from tests.common import snapshot_platform
@ -81,11 +85,13 @@ async def test_service_call_fail(
monkeypatch.setattr(
RemoteServices,
"trigger_remote_service",
AsyncMock(side_effect=MyBMWRemoteServiceError),
AsyncMock(
side_effect=MyBMWRemoteServiceError("HTTPStatusError: 502 Bad Gateway")
),
)
# Test
with pytest.raises(HomeAssistantError):
with pytest.raises(HomeAssistantError, match=REMOTE_SERVICE_EXC_TRANSLATION):
await hass.services.async_call(
"button",
"press",

View File

@ -16,7 +16,12 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
from . import check_remote_service_call, setup_mocked_integration
from . import (
REMOTE_SERVICE_EXC_REASON,
REMOTE_SERVICE_EXC_TRANSLATION,
check_remote_service_call,
setup_mocked_integration,
)
from tests.common import snapshot_platform
from tests.components.recorder.common import async_wait_recording_done
@ -118,11 +123,11 @@ async def test_service_call_fail(
monkeypatch.setattr(
RemoteServices,
"trigger_remote_service",
AsyncMock(side_effect=MyBMWRemoteServiceError),
AsyncMock(side_effect=MyBMWRemoteServiceError(REMOTE_SERVICE_EXC_REASON)),
)
# Test
with pytest.raises(HomeAssistantError):
with pytest.raises(HomeAssistantError, match=REMOTE_SERVICE_EXC_TRANSLATION):
await hass.services.async_call(
"lock",
service,

View File

@ -11,7 +11,11 @@ import respx
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from . import check_remote_service_call, setup_mocked_integration
from . import (
REMOTE_SERVICE_EXC_TRANSLATION,
check_remote_service_call,
setup_mocked_integration,
)
async def test_legacy_notify_service_simple(
@ -68,21 +72,21 @@ async def test_legacy_notify_service_simple(
{
"latitude": POI_DATA.get("lat"),
},
"Invalid data for point of interest: required key not provided @ data['longitude']",
r"Invalid data for point of interest: required key not provided @ data\['longitude'\]",
),
(
{
"latitude": POI_DATA.get("lat"),
"longitude": "text",
},
"Invalid data for point of interest: invalid longitude for dictionary value @ data['longitude']",
r"Invalid data for point of interest: invalid longitude for dictionary value @ data\['longitude'\]",
),
(
{
"latitude": POI_DATA.get("lat"),
"longitude": 9999,
},
"Invalid data for point of interest: invalid longitude for dictionary value @ data['longitude']",
r"Invalid data for point of interest: invalid longitude for dictionary value @ data\['longitude'\]",
),
],
)
@ -96,7 +100,7 @@ async def test_service_call_invalid_input(
# Setup component
assert await setup_mocked_integration(hass)
with pytest.raises(ServiceValidationError) as exc:
with pytest.raises(ServiceValidationError, match=exc_translation):
await hass.services.async_call(
"notify",
"bmw_connected_drive_ix_xdrive50",
@ -106,7 +110,6 @@ async def test_service_call_invalid_input(
},
blocking=True,
)
assert str(exc.value) == exc_translation
@pytest.mark.usefixtures("bmw_fixture")
@ -132,11 +135,11 @@ async def test_service_call_fail(
monkeypatch.setattr(
RemoteServices,
"trigger_remote_service",
AsyncMock(side_effect=raised),
AsyncMock(side_effect=raised("HTTPStatusError: 502 Bad Gateway")),
)
# Test
with pytest.raises(expected):
with pytest.raises(expected, match=REMOTE_SERVICE_EXC_TRANSLATION):
await hass.services.async_call(
"notify",
"bmw_connected_drive_ix_xdrive50",

View File

@ -13,7 +13,12 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import check_remote_service_call, setup_mocked_integration
from . import (
REMOTE_SERVICE_EXC_REASON,
REMOTE_SERVICE_EXC_TRANSLATION,
check_remote_service_call,
setup_mocked_integration,
)
from tests.common import snapshot_platform
@ -89,7 +94,10 @@ async def test_service_call_invalid_input(
old_value = hass.states.get(entity_id).state
# Test
with pytest.raises(ValueError):
with pytest.raises(
ValueError,
match="Target SoC must be an integer between 20 and 100 that is a multiple of 5.",
):
await hass.services.async_call(
"number",
"set_value",
@ -102,17 +110,32 @@ async def test_service_call_invalid_input(
@pytest.mark.usefixtures("bmw_fixture")
@pytest.mark.parametrize(
("raised", "expected"),
("raised", "expected", "exc_translation"),
[
(MyBMWRemoteServiceError, HomeAssistantError),
(MyBMWAPIError, HomeAssistantError),
(ValueError, ValueError),
(
MyBMWRemoteServiceError(REMOTE_SERVICE_EXC_REASON),
HomeAssistantError,
REMOTE_SERVICE_EXC_TRANSLATION,
),
(
MyBMWAPIError(REMOTE_SERVICE_EXC_REASON),
HomeAssistantError,
REMOTE_SERVICE_EXC_TRANSLATION,
),
(
ValueError(
"Target SoC must be an integer between 20 and 100 that is a multiple of 5."
),
ValueError,
"Target SoC must be an integer between 20 and 100 that is a multiple of 5.",
),
],
)
async def test_service_call_fail(
hass: HomeAssistant,
raised: Exception,
expected: Exception,
exc_translation: str,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test exception handling."""
@ -130,7 +153,7 @@ async def test_service_call_fail(
)
# Test
with pytest.raises(expected):
with pytest.raises(expected, match=exc_translation):
await hass.services.async_call(
"number",
"set_value",

View File

@ -16,7 +16,12 @@ 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
from . import (
REMOTE_SERVICE_EXC_REASON,
REMOTE_SERVICE_EXC_TRANSLATION,
check_remote_service_call,
setup_mocked_integration,
)
from tests.common import snapshot_platform
@ -105,7 +110,10 @@ async def test_service_call_invalid_input(
old_value = hass.states.get(entity_id).state
# Test
with pytest.raises(ServiceValidationError):
with pytest.raises(
ServiceValidationError,
match=f"Option {value} is not valid for entity {entity_id}",
):
await hass.services.async_call(
"select",
"select_option",
@ -118,17 +126,32 @@ async def test_service_call_invalid_input(
@pytest.mark.usefixtures("bmw_fixture")
@pytest.mark.parametrize(
("raised", "expected"),
("raised", "expected", "exc_translation"),
[
(MyBMWRemoteServiceError, HomeAssistantError),
(MyBMWAPIError, HomeAssistantError),
(ServiceValidationError, ServiceValidationError),
(
MyBMWRemoteServiceError(REMOTE_SERVICE_EXC_REASON),
HomeAssistantError,
REMOTE_SERVICE_EXC_TRANSLATION,
),
(
MyBMWAPIError(REMOTE_SERVICE_EXC_REASON),
HomeAssistantError,
REMOTE_SERVICE_EXC_TRANSLATION,
),
(
ServiceValidationError(
"Option 17 is not valid for entity select.i4_edrive40_ac_charging_limit"
),
ServiceValidationError,
"Option 17 is not valid for entity select.i4_edrive40_ac_charging_limit",
),
],
)
async def test_service_call_fail(
hass: HomeAssistant,
raised: Exception,
expected: Exception,
exc_translation: str,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test exception handling."""
@ -146,7 +169,7 @@ async def test_service_call_fail(
)
# Test
with pytest.raises(expected):
with pytest.raises(expected, match=exc_translation):
await hass.services.async_call(
"select",
"select_option",

View File

@ -13,7 +13,12 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import check_remote_service_call, setup_mocked_integration
from . import (
REMOTE_SERVICE_EXC_REASON,
REMOTE_SERVICE_EXC_TRANSLATION,
check_remote_service_call,
setup_mocked_integration,
)
from tests.common import snapshot_platform
@ -75,17 +80,25 @@ async def test_service_call_success(
@pytest.mark.usefixtures("bmw_fixture")
@pytest.mark.parametrize(
("raised", "expected"),
("raised", "expected", "exc_translation"),
[
(MyBMWRemoteServiceError, HomeAssistantError),
(MyBMWAPIError, HomeAssistantError),
(ValueError, ValueError),
(
MyBMWRemoteServiceError(REMOTE_SERVICE_EXC_REASON),
HomeAssistantError,
REMOTE_SERVICE_EXC_TRANSLATION,
),
(
MyBMWAPIError(REMOTE_SERVICE_EXC_REASON),
HomeAssistantError,
REMOTE_SERVICE_EXC_TRANSLATION,
),
],
)
async def test_service_call_fail(
hass: HomeAssistant,
raised: Exception,
expected: Exception,
exc_translation: str,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test exception handling."""
@ -107,7 +120,7 @@ async def test_service_call_fail(
assert hass.states.get(entity_id).state == old_value
# Test
with pytest.raises(expected):
with pytest.raises(expected, match=exc_translation):
await hass.services.async_call(
"switch",
"turn_on",
@ -122,7 +135,7 @@ async def test_service_call_fail(
assert hass.states.get(entity_id).state == old_value
# Test
with pytest.raises(expected):
with pytest.raises(expected, match=exc_translation):
await hass.services.async_call(
"switch",
"turn_off",