From 2d4c96864b136ecebeb107850e35387d6cfe462a Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 24 Mar 2026 08:20:36 +1000 Subject: [PATCH] Add exception translations to Tessie (#166047) --- .../components/tessie/coordinator.py | 27 +++++++++--- homeassistant/components/tessie/entity.py | 9 ++-- .../components/tessie/quality_scale.yaml | 8 +--- homeassistant/components/tessie/strings.json | 3 ++ tests/components/tessie/test_climate.py | 29 ++++++++++++- tests/components/tessie/test_coordinator.py | 41 ++++++++++++++++--- tests/components/tessie/test_cover.py | 2 + tests/components/tessie/test_select.py | 2 + 8 files changed, 99 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/tessie/coordinator.py b/homeassistant/components/tessie/coordinator.py index 97e94c25c7c..2a0c0e07f94 100644 --- a/homeassistant/components/tessie/coordinator.py +++ b/homeassistant/components/tessie/coordinator.py @@ -7,7 +7,7 @@ from http import HTTPStatus import logging from typing import TYPE_CHECKING, Any -from aiohttp import ClientResponseError +from aiohttp import ClientError, ClientResponseError from tesla_fleet_api.const import TeslaEnergyPeriod from tesla_fleet_api.exceptions import InvalidToken, MissingToken, TeslaFleetError from tesla_fleet_api.tessie import EnergySite @@ -83,7 +83,15 @@ class TessieStateUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): except ClientResponseError as e: if e.status == HTTPStatus.UNAUTHORIZED: raise ConfigEntryAuthFailed from e - raise + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="cannot_connect", + ) from e + except ClientError as e: + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="cannot_connect", + ) from e return flatten(vehicle) @@ -123,7 +131,10 @@ class TessieEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]]): except (InvalidToken, MissingToken) as e: raise ConfigEntryAuthFailed from e except TeslaFleetError as e: - raise UpdateFailed(e.message) from e + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="cannot_connect", + ) from e # Convert Wall Connectors from array to dict data["wall_connectors"] = { @@ -159,7 +170,10 @@ class TessieEnergySiteInfoCoordinator(DataUpdateCoordinator[dict[str, Any]]): except (InvalidToken, MissingToken) as e: raise ConfigEntryAuthFailed from e except TeslaFleetError as e: - raise UpdateFailed(e.message) from e + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="cannot_connect", + ) from e return flatten(data) @@ -197,7 +211,10 @@ class TessieEnergyHistoryCoordinator(DataUpdateCoordinator[dict[str, Any]]): translation_key="auth_failed", ) from e except TeslaFleetError as e: - raise UpdateFailed(e.message) from e + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="cannot_connect", + ) from e if ( not data diff --git a/homeassistant/components/tessie/entity.py b/homeassistant/components/tessie/entity.py index 95ba212e222..98a424eefc1 100644 --- a/homeassistant/components/tessie/entity.py +++ b/homeassistant/components/tessie/entity.py @@ -4,7 +4,7 @@ from abc import abstractmethod from collections.abc import Awaitable, Callable from typing import Any -from aiohttp import ClientResponseError +from aiohttp import ClientError from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo @@ -103,8 +103,11 @@ class TessieEntity(TessieBaseEntity): api_key=self._api_key, **kargs, ) - except ClientResponseError as e: - raise HomeAssistantError from e + except ClientError as e: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="cannot_connect", + ) from e if response["result"] is False: name: str = getattr(self, "name", self.entity_id) reason: str = response.get("reason", "unknown") diff --git a/homeassistant/components/tessie/quality_scale.yaml b/homeassistant/components/tessie/quality_scale.yaml index a814a4e0624..19fb4dbfe66 100644 --- a/homeassistant/components/tessie/quality_scale.yaml +++ b/homeassistant/components/tessie/quality_scale.yaml @@ -72,13 +72,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: - status: todo - comment: | - Most user-facing exceptions have translations (HomeAssistantError and - ServiceValidationError use translation keys from strings.json). Remaining: - entity.py raises bare HomeAssistantError for ClientResponseError, and - coordinators raise UpdateFailed with untranslated messages. + exception-translations: done icon-translations: done reconfiguration-flow: todo repair-issues: todo diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index 06516877db7..9be7124a85c 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -631,6 +631,9 @@ "cable_connected": { "message": "Charge cable is connected." }, + "cannot_connect": { + "message": "[%key:common::config_flow::error::cannot_connect%]" + }, "command_failed": { "message": "Command failed, {message}" }, diff --git a/tests/components/tessie/test_climate.py b/tests/components/tessie/test_climate.py index 87ba067ae5b..a9e3c958da1 100644 --- a/tests/components/tessie/test_climate.py +++ b/tests/components/tessie/test_climate.py @@ -22,7 +22,13 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from .common import ERROR_UNKNOWN, TEST_RESPONSE, assert_entities, setup_platform +from .common import ( + ERROR_CONNECTION, + ERROR_UNKNOWN, + TEST_RESPONSE, + assert_entities, + setup_platform, +) async def test_climate( @@ -130,6 +136,27 @@ async def test_errors(hass: HomeAssistant) -> None: ) mock_set.assert_called_once() assert error.value.__cause__ == ERROR_UNKNOWN + assert error.value.translation_domain == "tessie" + assert error.value.translation_key == "cannot_connect" + + # Test setting climate on with connection error + with ( + patch( + "homeassistant.components.tessie.climate.stop_climate", + side_effect=ERROR_CONNECTION, + ) as mock_set, + pytest.raises(HomeAssistantError) as error, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + ) + mock_set.assert_called_once() + assert error.value.__cause__ == ERROR_CONNECTION + assert error.value.translation_domain == "tessie" + assert error.value.translation_key == "cannot_connect" # Test setting climate with child presence detection error with ( diff --git a/tests/components/tessie/test_coordinator.py b/tests/components/tessie/test_coordinator.py index 89278e64ede..91b7c233135 100644 --- a/tests/components/tessie/test_coordinator.py +++ b/tests/components/tessie/test_coordinator.py @@ -7,6 +7,7 @@ import pytest from tesla_fleet_api.exceptions import Forbidden, InvalidToken, MissingToken from homeassistant.components.tessie import PLATFORMS +from homeassistant.components.tessie.const import DOMAIN from homeassistant.components.tessie.coordinator import ( TESSIE_ENERGY_HISTORY_INTERVAL, TESSIE_FLEET_API_SYNC_INTERVAL, @@ -15,6 +16,7 @@ from homeassistant.components.tessie.coordinator import ( from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import UpdateFailed from .common import ERROR_AUTH, ERROR_CONNECTION, ERROR_UNKNOWN, setup_platform @@ -43,13 +45,17 @@ async def test_coordinator_clienterror( """Tests that the coordinator handles client errors.""" mock_get_state.side_effect = ERROR_UNKNOWN - await setup_platform(hass, [Platform.BINARY_SENSOR]) + entry = await setup_platform(hass, [Platform.BINARY_SENSOR]) + coordinator = entry.runtime_data.vehicles[0].data_coordinator freezer.tick(WAIT) async_fire_time_changed(hass) await hass.async_block_till_done() mock_get_state.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_UNAVAILABLE + assert isinstance(coordinator.last_exception, UpdateFailed) + assert coordinator.last_exception.translation_domain == DOMAIN + assert coordinator.last_exception.translation_key == "cannot_connect" async def test_coordinator_auth( @@ -72,12 +78,16 @@ async def test_coordinator_connection( """Tests that the coordinator handles connection errors.""" mock_get_state.side_effect = ERROR_CONNECTION - await setup_platform(hass, [Platform.BINARY_SENSOR]) + entry = await setup_platform(hass, [Platform.BINARY_SENSOR]) + coordinator = entry.runtime_data.vehicles[0].data_coordinator freezer.tick(WAIT) async_fire_time_changed(hass) await hass.async_block_till_done() mock_get_state.assert_called_once() assert hass.states.get("binary_sensor.test_status").state == STATE_UNAVAILABLE + assert isinstance(coordinator.last_exception, UpdateFailed) + assert coordinator.last_exception.translation_domain == DOMAIN + assert coordinator.last_exception.translation_key == "cannot_connect" async def test_coordinator_live_error( @@ -85,7 +95,9 @@ async def test_coordinator_live_error( ) -> None: """Tests that the energy live coordinator handles fleet errors.""" - await setup_platform(hass, [Platform.SENSOR]) + entry = await setup_platform(hass, [Platform.SENSOR]) + coordinator = entry.runtime_data.energysites[0].live_coordinator + assert coordinator is not None mock_live_status.reset_mock() mock_live_status.side_effect = Forbidden @@ -94,6 +106,9 @@ async def test_coordinator_live_error( await hass.async_block_till_done() mock_live_status.assert_called_once() assert hass.states.get("sensor.energy_site_solar_power").state == STATE_UNAVAILABLE + assert isinstance(coordinator.last_exception, UpdateFailed) + assert coordinator.last_exception.translation_domain == DOMAIN + assert coordinator.last_exception.translation_key == "cannot_connect" async def test_coordinator_info_error( @@ -101,7 +116,8 @@ async def test_coordinator_info_error( ) -> None: """Tests that the energy info coordinator handles fleet errors.""" - await setup_platform(hass, [Platform.SENSOR]) + entry = await setup_platform(hass, [Platform.SENSOR]) + coordinator = entry.runtime_data.energysites[0].info_coordinator mock_site_info.reset_mock() mock_site_info.side_effect = Forbidden @@ -113,6 +129,9 @@ async def test_coordinator_info_error( hass.states.get("sensor.energy_site_vpp_backup_reserve").state == STATE_UNAVAILABLE ) + assert isinstance(coordinator.last_exception, UpdateFailed) + assert coordinator.last_exception.translation_domain == DOMAIN + assert coordinator.last_exception.translation_key == "cannot_connect" @pytest.mark.parametrize( @@ -144,7 +163,9 @@ async def test_coordinator_energy_history_error( ) -> None: """Tests that the energy history coordinator handles fleet errors.""" - await setup_platform(hass, [Platform.SENSOR]) + entry = await setup_platform(hass, [Platform.SENSOR]) + coordinator = entry.runtime_data.energysites[0].history_coordinator + assert coordinator is not None mock_energy_history.reset_mock() mock_energy_history.side_effect = Forbidden @@ -155,6 +176,9 @@ async def test_coordinator_energy_history_error( assert ( hass.states.get("sensor.energy_site_grid_imported").state == STATE_UNAVAILABLE ) + assert isinstance(coordinator.last_exception, UpdateFailed) + assert coordinator.last_exception.translation_domain == DOMAIN + assert coordinator.last_exception.translation_key == "cannot_connect" async def test_coordinator_energy_history_invalid_data( @@ -162,7 +186,9 @@ async def test_coordinator_energy_history_invalid_data( ) -> None: """Tests that the energy history coordinator handles invalid data.""" - await setup_platform(hass, [Platform.SENSOR]) + entry = await setup_platform(hass, [Platform.SENSOR]) + coordinator = entry.runtime_data.energysites[0].history_coordinator + assert coordinator is not None mock_energy_history.reset_mock() mock_energy_history.side_effect = lambda *a, **kw: {"response": {}} @@ -173,3 +199,6 @@ async def test_coordinator_energy_history_invalid_data( assert ( hass.states.get("sensor.energy_site_grid_imported").state == STATE_UNAVAILABLE ) + assert isinstance(coordinator.last_exception, UpdateFailed) + assert coordinator.last_exception.translation_domain == DOMAIN + assert coordinator.last_exception.translation_key == "invalid_energy_history_data" diff --git a/tests/components/tessie/test_cover.py b/tests/components/tessie/test_cover.py index b71b1f44377..02e05cc3ef0 100644 --- a/tests/components/tessie/test_cover.py +++ b/tests/components/tessie/test_cover.py @@ -96,6 +96,8 @@ async def test_errors(hass: HomeAssistant) -> None: ) mock_set.assert_called_once() assert error.value.__cause__ == ERROR_UNKNOWN + assert error.value.translation_domain == "tessie" + assert error.value.translation_key == "cannot_connect" # Test setting cover open with unknown error with ( diff --git a/tests/components/tessie/test_select.py b/tests/components/tessie/test_select.py index 44a5e99b5c1..03331d9abb3 100644 --- a/tests/components/tessie/test_select.py +++ b/tests/components/tessie/test_select.py @@ -125,6 +125,8 @@ async def test_errors(hass: HomeAssistant) -> None: ) mock_set.assert_called_once() assert error.value.__cause__ == ERROR_UNKNOWN + assert error.value.translation_domain == "tessie" + assert error.value.translation_key == "cannot_connect" # Test changing energy select with unknown error with (