From 0f1835139f04d75d6de56fc8b279e41743180e7f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Dec 2024 19:53:00 +0100 Subject: [PATCH] Add number error handling for Peblar Rocksolid EV Chargers (#133803) --- homeassistant/components/peblar/number.py | 2 + tests/components/peblar/test_number.py | 148 +++++++++++++++++++++- 2 files changed, 147 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/peblar/number.py b/homeassistant/components/peblar/number.py index d17ff09eb94..d2983438a91 100644 --- a/homeassistant/components/peblar/number.py +++ b/homeassistant/components/peblar/number.py @@ -24,6 +24,7 @@ from .coordinator import ( PeblarRuntimeData, ) from .entity import PeblarEntity +from .helpers import peblar_exception_handler PARALLEL_UPDATES = 1 @@ -94,6 +95,7 @@ class PeblarNumberEntity( """Return the number value.""" return self.entity_description.value_fn(self.coordinator.data) + @peblar_exception_handler async def async_set_native_value(self, value: float) -> None: """Change to new number value.""" await self.entity_description.set_value_fn(self.coordinator.api, value) diff --git a/tests/components/peblar/test_number.py b/tests/components/peblar/test_number.py index 4c2ff928210..2a8fca46e91 100644 --- a/tests/components/peblar/test_number.py +++ b/tests/components/peblar/test_number.py @@ -1,18 +1,31 @@ """Tests for the Peblar number platform.""" +from unittest.mock import MagicMock + +from peblar import PeblarAuthenticationError, PeblarConnectionError, PeblarError import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) from homeassistant.components.peblar.const import DOMAIN -from homeassistant.const import Platform +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry, snapshot_platform +pytestmark = [ + pytest.mark.parametrize("init_integration", [Platform.NUMBER], indirect=True), + pytest.mark.usefixtures("init_integration"), +] + -@pytest.mark.parametrize("init_integration", [Platform.NUMBER], indirect=True) -@pytest.mark.usefixtures("init_integration") async def test_entities( hass: HomeAssistant, snapshot: SnapshotAssertion, @@ -33,3 +46,132 @@ async def test_entities( ) for entity_entry in entity_entries: assert entity_entry.device_id == device_entry.id + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_number_set_value( + hass: HomeAssistant, + mock_peblar: MagicMock, +) -> None: + """Test the Peblar EV charger numbers.""" + entity_id = "number.peblar_ev_charger_charge_limit" + mocked_method = mock_peblar.rest_api.return_value.ev_interface + mocked_method.reset_mock() + + # Test normal happy path number value change + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: 10, + }, + blocking=True, + ) + + assert len(mocked_method.mock_calls) == 2 + mocked_method.mock_calls[0].assert_called_with({"charge_current_limit": 10}) + + +@pytest.mark.parametrize( + ("error", "error_match", "translation_key", "translation_placeholders"), + [ + ( + PeblarConnectionError("Could not connect"), + ( + r"An error occurred while communicating " + r"with the Peblar device: Could not connect" + ), + "communication_error", + {"error": "Could not connect"}, + ), + ( + PeblarError("Unknown error"), + ( + r"An unknown error occurred while communicating " + r"with the Peblar device: Unknown error" + ), + "unknown_error", + {"error": "Unknown error"}, + ), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_number_set_value_communication_error( + hass: HomeAssistant, + mock_peblar: MagicMock, + error: Exception, + error_match: str, + translation_key: str, + translation_placeholders: dict, +) -> None: + """Test the Peblar EV charger when a communication error occurs.""" + entity_id = "number.peblar_ev_charger_charge_limit" + mock_peblar.rest_api.return_value.ev_interface.side_effect = error + + with pytest.raises( + HomeAssistantError, + match=error_match, + ) as excinfo: + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: 10, + }, + blocking=True, + ) + + assert excinfo.value.translation_domain == DOMAIN + assert excinfo.value.translation_key == translation_key + assert excinfo.value.translation_placeholders == translation_placeholders + + +async def test_number_set_value_authentication_error( + hass: HomeAssistant, + mock_peblar: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the Peblar EV charger when an authentication error occurs.""" + entity_id = "number.peblar_ev_charger_charge_limit" + mock_peblar.rest_api.return_value.ev_interface.side_effect = ( + PeblarAuthenticationError("Authentication error") + ) + mock_peblar.login.side_effect = PeblarAuthenticationError("Authentication error") + with pytest.raises( + HomeAssistantError, + match=( + r"An authentication failure occurred while communicating " + r"with the Peblar device" + ), + ) as excinfo: + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: 10, + }, + blocking=True, + ) + + assert excinfo.value.translation_domain == DOMAIN + assert excinfo.value.translation_key == "authentication_error" + assert not excinfo.value.translation_placeholders + + # Ensure the device is reloaded on authentication error and triggers + # a reauthentication flow. + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == mock_config_entry.entry_id