From 80632caf2eadb5a154f1c53990e02264ca79768d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sun, 6 Sep 2026 20:45:42 +0200 Subject: [PATCH] Homevolt error handling (#181463) --- homeassistant/components/homevolt/entity.py | 26 +++++++- .../components/homevolt/strings.json | 9 +++ tests/components/homevolt/test_switch.py | 62 ++++++++++++++++++- 3 files changed, 94 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homevolt/entity.py b/homeassistant/components/homevolt/entity.py index fcea7c6da3ac..098d895fa29e 100644 --- a/homeassistant/components/homevolt/entity.py +++ b/homeassistant/components/homevolt/entity.py @@ -3,7 +3,14 @@ from collections.abc import Callable, Coroutine from typing import Any, Concatenate -from homevolt import HomevoltAuthenticationError, HomevoltConnectionError, HomevoltError +from homevolt import ( + HomevoltAuthenticationError, + HomevoltCommandOutcomeUnknownError, + HomevoltCommandRejectedError, + HomevoltCommandVerificationError, + HomevoltConnectionError, + HomevoltError, +) from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo @@ -49,6 +56,23 @@ def homevolt_exception_handler[_HomevoltEntityT: HomevoltEntity, **_P]( translation_domain=DOMAIN, translation_key="auth_failed", ) from error + except HomevoltCommandRejectedError as error: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="command_rejected", + ) from error + except HomevoltCommandVerificationError as error: + await self.coordinator.async_refresh() + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="command_verification_failed", + ) from error + except HomevoltCommandOutcomeUnknownError as error: + await self.coordinator.async_refresh() + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="command_outcome_unknown", + ) from error except HomevoltConnectionError as error: raise HomeAssistantError( translation_domain=DOMAIN, diff --git a/homeassistant/components/homevolt/strings.json b/homeassistant/components/homevolt/strings.json index 6f4915beb0cd..6561c757a3db 100644 --- a/homeassistant/components/homevolt/strings.json +++ b/homeassistant/components/homevolt/strings.json @@ -170,6 +170,15 @@ "auth_failed": { "message": "[%key:common::config_flow::error::invalid_auth%]" }, + "command_outcome_unknown": { + "message": "The Homevolt command was sent, but its result could not be verified." + }, + "command_rejected": { + "message": "The Homevolt battery rejected the command." + }, + "command_verification_failed": { + "message": "The Homevolt battery did not apply the requested command." + }, "communication_error": { "message": "Error communicating with the Homevolt battery: {error}" }, diff --git a/tests/components/homevolt/test_switch.py b/tests/components/homevolt/test_switch.py index 37227a483f9b..dc772710f825 100644 --- a/tests/components/homevolt/test_switch.py +++ b/tests/components/homevolt/test_switch.py @@ -2,10 +2,18 @@ from unittest.mock import MagicMock -from homevolt import HomevoltAuthenticationError, HomevoltConnectionError, HomevoltError +from homevolt import ( + HomevoltAuthenticationError, + HomevoltCommandOutcomeUnknownError, + HomevoltCommandRejectedError, + HomevoltCommandVerificationError, + HomevoltConnectionError, + HomevoltError, +) import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.homevolt.const import DOMAIN from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_OFF, @@ -128,11 +136,11 @@ async def test_switch_turn_on_off( async def test_switch_turn_on_off_exception_handler( hass: HomeAssistant, mock_homevolt_client: MagicMock, - switch_entity_id: str, service: str, client_method_name: str, exception: Exception, expected_exception: type[Exception], + switch_entity_id: str, ) -> None: """Test homevolt_exception_handler raises correct exception on turn_on/turn_off.""" getattr(mock_homevolt_client, client_method_name).side_effect = exception @@ -144,3 +152,53 @@ async def test_switch_turn_on_off_exception_handler( {ATTR_ENTITY_ID: switch_entity_id}, blocking=True, ) + + +@pytest.mark.parametrize( + ("exception", "translation_key", "refresh_count"), + [ + pytest.param( + HomevoltCommandRejectedError("invalid command"), + "command_rejected", + 0, + id="rejected", + ), + pytest.param( + HomevoltCommandVerificationError("state mismatch"), + "command_verification_failed", + 1, + id="verification", + ), + pytest.param( + HomevoltCommandOutcomeUnknownError("read-back failed"), + "command_outcome_unknown", + 1, + id="outcome-unknown", + ), + ], +) +async def test_switch_command_error( + hass: HomeAssistant, + mock_homevolt_client: MagicMock, + switch_entity_id: str, + exception: HomevoltError, + translation_key: str, + refresh_count: int, +) -> None: + """Test command errors are translated and uncertain state is refreshed.""" + mock_homevolt_client.enable_local_mode.side_effect = exception + mock_homevolt_client.update_info.reset_mock() + + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: switch_entity_id}, + blocking=True, + ) + + assert exc_info.value.translation_domain == DOMAIN + assert exc_info.value.translation_key == translation_key + assert exc_info.value.translation_placeholders is None + assert exc_info.value.__cause__ is exception + assert mock_homevolt_client.update_info.await_count == refresh_count