From ec0f7aea755e1bfe37ad9344c099dfcfc7def2bb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 2 Oct 2026 21:45:09 +0200 Subject: [PATCH] Raise translated errors from Electric Kiwi hour of free power select (#184054) Co-authored-by: Claude Opus 5.5 --- .../components/electric_kiwi/coordinator.py | 25 +++-- .../electric_kiwi/quality_scale.yaml | 5 +- .../components/electric_kiwi/strings.json | 8 ++ tests/components/electric_kiwi/test_select.py | 106 ++++++++++++++++++ 4 files changed, 133 insertions(+), 11 deletions(-) create mode 100644 tests/components/electric_kiwi/test_select.py diff --git a/homeassistant/components/electric_kiwi/coordinator.py b/homeassistant/components/electric_kiwi/coordinator.py index 934deb0f98ba..dac0fd7fb80c 100644 --- a/homeassistant/components/electric_kiwi/coordinator.py +++ b/homeassistant/components/electric_kiwi/coordinator.py @@ -7,15 +7,18 @@ from datetime import timedelta import logging from typing import override +from aiohttp import ClientError from electrickiwi_api import ElectricKiwiApi from electrickiwi_api.exceptions import ApiException, AuthException from electrickiwi_api.model import AccountSummary, Hop, HopIntervals from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from .const import DOMAIN + _LOGGER = logging.getLogger(__name__) ACCOUNT_SCAN_INTERVAL = timedelta(hours=6) @@ -69,6 +72,8 @@ class ElectricKiwiAccountDataCoordinator(DataUpdateCoordinator[AccountSummary]): class ElectricKiwiHOPDataCoordinator(DataUpdateCoordinator[Hop]): """ElectricKiwi HOP Data object.""" + config_entry: ElectricKiwiConfigEntry + def __init__( self, hass: HomeAssistant, @@ -100,14 +105,20 @@ class ElectricKiwiHOPDataCoordinator(DataUpdateCoordinator[Hop]): async def async_update_hop(self, hop_interval: int) -> Hop: """Update selected hop and data.""" try: - self.async_set_updated_data(await self.ek_api.post_hop(hop_interval)) + hop = await self.ek_api.post_hop(hop_interval) except AuthException as auth_err: - raise ConfigEntryAuthFailed from auth_err - except ApiException as api_err: - raise UpdateFailed( - f"Error communicating with EK API: {api_err}" - ) from api_err + self.config_entry.async_start_reauth(self.hass) + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="auth_failed", + ) from auth_err + except (ApiException, ClientError, TimeoutError) as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="set_hop_failed", + ) from err + self.async_set_updated_data(hop) return self.data @override diff --git a/homeassistant/components/electric_kiwi/quality_scale.yaml b/homeassistant/components/electric_kiwi/quality_scale.yaml index 23e8428ad635..f8fd23aa58f4 100644 --- a/homeassistant/components/electric_kiwi/quality_scale.yaml +++ b/homeassistant/components/electric_kiwi/quality_scale.yaml @@ -35,10 +35,7 @@ rules: unique-config-entry: done # Silver - action-exceptions: - status: exempt - comment: | - No actions + action-exceptions: done config-entry-unloading: done docs-configuration-parameters: status: exempt diff --git a/homeassistant/components/electric_kiwi/strings.json b/homeassistant/components/electric_kiwi/strings.json index 05e879f20c87..cbfae05dc13d 100644 --- a/homeassistant/components/electric_kiwi/strings.json +++ b/homeassistant/components/electric_kiwi/strings.json @@ -46,5 +46,13 @@ "name": "Total running balance" } } + }, + "exceptions": { + "auth_failed": { + "message": "Authentication with Electric Kiwi failed. Please re-authenticate." + }, + "set_hop_failed": { + "message": "Failed to set the hour of free power." + } } } diff --git a/tests/components/electric_kiwi/test_select.py b/tests/components/electric_kiwi/test_select.py new file mode 100644 index 000000000000..7eb40309d755 --- /dev/null +++ b/tests/components/electric_kiwi/test_select.py @@ -0,0 +1,106 @@ +"""The tests for Electric Kiwi select.""" + +from unittest.mock import AsyncMock + +from aiohttp import ClientError +from electrickiwi_api.exceptions import ApiException, AuthException +from electrickiwi_api.model import Hop +import pytest + +from homeassistant.components.electric_kiwi.const import DOMAIN +from homeassistant.components.select import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from . import init_integration + +from tests.common import MockConfigEntry, load_json_value_fixture + +ENTITY_ID = "select.hour_of_free_power" +OPTION = "12:00 AM - 1:00 AM" + + +@pytest.mark.usefixtures("ek_auth") +async def test_select_option( + hass: HomeAssistant, + config_entry: MockConfigEntry, + electrickiwi_api: AsyncMock, +) -> None: + """Test selecting a new hour of free power.""" + await init_integration(hass, config_entry) + hop_data = load_json_value_fixture("get_hop.json", DOMAIN) + hop_data["data"]["start"] = {"interval": "1", "start_time": "12:00 AM"} + hop_data["data"]["end"] = {"interval": "2", "end_time": "1:00 AM"} + electrickiwi_api.post_hop.return_value = Hop.from_dict(hop_data) + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_OPTION: OPTION}, + blocking=True, + ) + + electrickiwi_api.post_hop.assert_awaited_once_with(1) + assert (state := hass.states.get(ENTITY_ID)) + assert state.state == OPTION + + +@pytest.mark.parametrize( + "exception", + [ + pytest.param(ApiException("API error"), id="api_error"), + pytest.param(ClientError("Connection error"), id="client_error"), + pytest.param(TimeoutError, id="timeout"), + ], +) +@pytest.mark.usefixtures("ek_auth") +async def test_select_option_error( + hass: HomeAssistant, + config_entry: MockConfigEntry, + electrickiwi_api: AsyncMock, + exception: Exception, +) -> None: + """Test errors when selecting an hour of free power.""" + await init_integration(hass, config_entry) + electrickiwi_api.post_hop.side_effect = exception + + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_OPTION: OPTION}, + blocking=True, + ) + + assert exc_info.value.translation_key == "set_hop_failed" + assert not hass.config_entries.flow.async_progress() + + +@pytest.mark.usefixtures("ek_auth") +async def test_select_option_auth_error( + hass: HomeAssistant, + config_entry: MockConfigEntry, + electrickiwi_api: AsyncMock, +) -> None: + """Test an auth error when selecting an hour of free power starts reauth.""" + await init_integration(hass, config_entry) + electrickiwi_api.post_hop.side_effect = AuthException("Auth error") + + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_OPTION: OPTION}, + blocking=True, + ) + + assert exc_info.value.translation_key == "auth_failed" + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["source"] == SOURCE_REAUTH