diff --git a/homeassistant/components/actron_air/climate.py b/homeassistant/components/actron_air/climate.py index e998902c002..8c928fcc5a9 100644 --- a/homeassistant/components/actron_air/climate.py +++ b/homeassistant/components/actron_air/climate.py @@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator -from .entity import ActronAirAcEntity, ActronAirZoneEntity +from .entity import ActronAirAcEntity, ActronAirZoneEntity, handle_actron_api_errors PARALLEL_UPDATES = 0 @@ -136,16 +136,19 @@ class ActronSystemClimate(ActronAirAcEntity, ActronAirClimateEntity): """Return the target temperature.""" return self._status.user_aircon_settings.temperature_setpoint_cool_c + @handle_actron_api_errors async def async_set_fan_mode(self, fan_mode: str) -> None: """Set a new fan mode.""" api_fan_mode = FAN_MODE_MAPPING_HA_TO_ACTRONAIR.get(fan_mode) await self._status.user_aircon_settings.set_fan_mode(api_fan_mode) + @handle_actron_api_errors async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the HVAC mode.""" ac_mode = HVAC_MODE_MAPPING_HA_TO_ACTRONAIR.get(hvac_mode) await self._status.ac_system.set_system_mode(ac_mode) + @handle_actron_api_errors async def async_set_temperature(self, **kwargs: Any) -> None: """Set the temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) @@ -209,11 +212,13 @@ class ActronZoneClimate(ActronAirZoneEntity, ActronAirClimateEntity): """Return the target temperature.""" return self._zone.temperature_setpoint_cool_c + @handle_actron_api_errors async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the HVAC mode.""" is_enabled = hvac_mode != HVACMode.OFF await self._zone.enable(is_enabled) + @handle_actron_api_errors async def async_set_temperature(self, **kwargs: Any) -> None: """Set the temperature.""" await self._zone.set_temperature(temperature=kwargs.get(ATTR_TEMPERATURE)) diff --git a/homeassistant/components/actron_air/entity.py b/homeassistant/components/actron_air/entity.py index 1c13f17d8c4..7f62c53516e 100644 --- a/homeassistant/components/actron_air/entity.py +++ b/homeassistant/components/actron_air/entity.py @@ -1,7 +1,12 @@ """Base entity classes for Actron Air integration.""" -from actron_neo_api import ActronAirZone +from collections.abc import Callable, Coroutine +from functools import wraps +from typing import Any, Concatenate +from actron_neo_api import ActronAirAPIError, ActronAirZone + +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -9,6 +14,26 @@ from .const import DOMAIN from .coordinator import ActronAirSystemCoordinator +def handle_actron_api_errors[_EntityT: ActronAirEntity, **_P]( + func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]], +) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]: + """Decorate Actron Air API calls to handle ActronAirAPIError exceptions.""" + + @wraps(func) + async def wrapper(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None: + """Wrap API calls with exception handling.""" + try: + await func(self, *args, **kwargs) + except ActronAirAPIError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="api_error", + translation_placeholders={"error": str(err)}, + ) from err + + return wrapper + + class ActronAirEntity(CoordinatorEntity[ActronAirSystemCoordinator]): """Base class for Actron Air entities.""" diff --git a/homeassistant/components/actron_air/quality_scale.yaml b/homeassistant/components/actron_air/quality_scale.yaml index 5d1f917da3b..cb608240459 100644 --- a/homeassistant/components/actron_air/quality_scale.yaml +++ b/homeassistant/components/actron_air/quality_scale.yaml @@ -26,7 +26,7 @@ rules: unique-config-entry: done # Silver - action-exceptions: todo + action-exceptions: done config-entry-unloading: done docs-configuration-parameters: status: exempt diff --git a/homeassistant/components/actron_air/strings.json b/homeassistant/components/actron_air/strings.json index 00ca2073179..9e22a6ffb86 100644 --- a/homeassistant/components/actron_air/strings.json +++ b/homeassistant/components/actron_air/strings.json @@ -49,6 +49,9 @@ } }, "exceptions": { + "api_error": { + "message": "Failed to communicate with Actron Air device: {error}" + }, "auth_error": { "message": "Authentication failed, please reauthenticate" }, diff --git a/homeassistant/components/actron_air/switch.py b/homeassistant/components/actron_air/switch.py index c7c9538675b..44efe6c9f74 100644 --- a/homeassistant/components/actron_air/switch.py +++ b/homeassistant/components/actron_air/switch.py @@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator -from .entity import ActronAirAcEntity +from .entity import ActronAirAcEntity, handle_actron_api_errors PARALLEL_UPDATES = 0 @@ -105,10 +105,12 @@ class ActronAirSwitch(ActronAirAcEntity, SwitchEntity): """Return true if the switch is on.""" return self.entity_description.is_on_fn(self.coordinator) + @handle_actron_api_errors async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self.entity_description.set_fn(self.coordinator, True) + @handle_actron_api_errors async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self.entity_description.set_fn(self.coordinator, False) diff --git a/tests/components/actron_air/conftest.py b/tests/components/actron_air/conftest.py index 6f1b4869882..0b4f2002938 100644 --- a/tests/components/actron_air/conftest.py +++ b/tests/components/actron_air/conftest.py @@ -7,7 +7,10 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from homeassistant.components.actron_air.const import DOMAIN -from homeassistant.const import CONF_API_TOKEN +from homeassistant.const import CONF_API_TOKEN, Platform +from homeassistant.core import HomeAssistant + +from . import setup_integration from tests.common import MockConfigEntry @@ -62,11 +65,14 @@ def mock_actron_api() -> Generator[AsyncMock]: api.state_manager = MagicMock() status = api.state_manager.get_status.return_value status.master_info.live_temp_c = 22.0 + status.master_info.live_humidity_pc = 50.0 status.ac_system.system_name = "Test System" status.ac_system.serial_number = "123456" status.ac_system.master_wc_model = "Test Model" status.ac_system.master_wc_firmware_version = "1.0.0" + status.ac_system.set_system_mode = AsyncMock() status.remote_zone_info = [] + status.zones = {} status.min_temp = 16 status.max_temp = 30 status.aircon_system.mode = "OFF" @@ -75,18 +81,27 @@ def mock_actron_api() -> Generator[AsyncMock]: status.room_temp = 25 status.is_on = False - # Mock user_aircon_settings for the switch platform + # Mock user_aircon_settings for the switch and climate platforms settings = status.user_aircon_settings settings.away_mode = False settings.continuous_fan_enabled = False settings.quiet_mode_enabled = False settings.turbo_enabled = False settings.turbo_supported = True + settings.is_on = False + settings.mode = "COOL" + settings.base_fan_mode = "LOW" + settings.temperature_setpoint_cool_c = 24.0 settings.set_away_mode = AsyncMock() settings.set_continuous_mode = AsyncMock() settings.set_quiet_mode = AsyncMock() settings.set_turbo_mode = AsyncMock() + settings.set_temperature = AsyncMock() + settings.set_fan_mode = AsyncMock() + + # Mock ac_system methods for climate platform + status.ac_system.set_system_mode = AsyncMock() yield api @@ -102,6 +117,26 @@ def mock_config_entry() -> MockConfigEntry: ) +@pytest.fixture +def mock_zone() -> MagicMock: + """Return a mocked zone.""" + zone = MagicMock() + zone.exists = True + zone.zone_id = 1 + zone.zone_name = "Test Zone" + zone.title = "Living Room" + zone.live_temp_c = 22.0 + zone.temperature_setpoint_cool_c = 24.0 + zone.is_active = True + zone.hvac_mode = "COOL" + zone.humidity = 50.0 + zone.min_temp = 16 + zone.max_temp = 30 + zone.set_temperature = AsyncMock() + zone.enable = AsyncMock() + return zone + + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: """Mock async_setup_entry.""" @@ -109,3 +144,19 @@ def mock_setup_entry() -> Generator[AsyncMock]: "homeassistant.components.actron_air.async_setup_entry", return_value=True ) as mock_setup: yield mock_setup + + +@pytest.fixture +async def init_integration_with_zone( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_actron_api: AsyncMock, + mock_zone: MagicMock, +) -> None: + """Set up the Actron Air integration with zone for testing.""" + status = mock_actron_api.state_manager.get_status.return_value + status.remote_zone_info = [mock_zone] + status.zones = {1: mock_zone} + + with patch("homeassistant.components.actron_air.PLATFORMS", [Platform.CLIMATE]): + await setup_integration(hass, mock_config_entry) diff --git a/tests/components/actron_air/snapshots/test_climate.ambr b/tests/components/actron_air/snapshots/test_climate.ambr new file mode 100644 index 00000000000..1c5958d7c5c --- /dev/null +++ b/tests/components/actron_air/snapshots/test_climate.ambr @@ -0,0 +1,158 @@ +# serializer version: 1 +# name: test_climate_entities[climate.living_room-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + , + , + ]), + 'max_temp': 30, + 'min_temp': 16, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.living_room', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'actron_air', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '123456_zone_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_entities[climate.living_room-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_humidity': 50.0, + 'current_temperature': 22.0, + 'friendly_name': 'Living Room', + 'hvac_modes': list([ + , + , + , + , + , + ]), + 'max_temp': 30, + 'min_temp': 16, + 'supported_features': , + 'temperature': 24.0, + }), + 'context': , + 'entity_id': 'climate.living_room', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- +# name: test_climate_entities[climate.test_system-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'fan_modes': list([ + 'auto', + 'low', + 'medium', + 'high', + ]), + 'hvac_modes': list([ + , + , + , + , + , + ]), + 'max_temp': 30, + 'min_temp': 16, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.test_system', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'actron_air', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '123456', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_entities[climate.test_system-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_humidity': 50.0, + 'current_temperature': 22.0, + 'fan_mode': 'low', + 'fan_modes': list([ + 'auto', + 'low', + 'medium', + 'high', + ]), + 'friendly_name': 'Test System', + 'hvac_modes': list([ + , + , + , + , + , + ]), + 'max_temp': 30, + 'min_temp': 16, + 'supported_features': , + 'temperature': 24.0, + }), + 'context': , + 'entity_id': 'climate.test_system', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/actron_air/test_climate.py b/tests/components/actron_air/test_climate.py new file mode 100644 index 00000000000..da471a3154a --- /dev/null +++ b/tests/components/actron_air/test_climate.py @@ -0,0 +1,260 @@ +"""Tests for the Actron Air climate platform.""" + +from unittest.mock import MagicMock, patch + +from actron_neo_api import ActronAirAPIError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.climate import ( + ATTR_FAN_MODE, + ATTR_HVAC_MODE, + DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_TEMPERATURE, + HVACMode, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_climate_entities( + hass: HomeAssistant, + mock_actron_api: MagicMock, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, + mock_zone: MagicMock, +) -> None: + """Test climate entities.""" + status = mock_actron_api.state_manager.get_status.return_value + status.remote_zone_info = [mock_zone] + status.zones = {1: mock_zone} + + with patch("homeassistant.components.actron_air.PLATFORMS", [Platform.CLIMATE]): + await setup_integration(hass, mock_config_entry) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_system_set_temperature( + hass: HomeAssistant, + mock_actron_api: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test setting temperature for system climate entity.""" + with patch("homeassistant.components.actron_air.PLATFORMS", [Platform.CLIMATE]): + await setup_integration(hass, mock_config_entry) + + status = mock_actron_api.state_manager.get_status.return_value + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: "climate.test_system", ATTR_TEMPERATURE: 22.5}, + blocking=True, + ) + + status.user_aircon_settings.set_temperature.assert_awaited_once_with( + temperature=22.5 + ) + + +async def test_system_set_temperature_api_error( + hass: HomeAssistant, + mock_actron_api: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test API error when setting temperature for system climate entity.""" + with patch("homeassistant.components.actron_air.PLATFORMS", [Platform.CLIMATE]): + await setup_integration(hass, mock_config_entry) + + status = mock_actron_api.state_manager.get_status.return_value + status.user_aircon_settings.set_temperature.side_effect = ActronAirAPIError( + "Test error" + ) + + with pytest.raises(HomeAssistantError, match="Test error"): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: "climate.test_system", ATTR_TEMPERATURE: 22.5}, + blocking=True, + ) + + +async def test_system_set_fan_mode( + hass: HomeAssistant, + mock_actron_api: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test setting fan mode for system climate entity.""" + with patch("homeassistant.components.actron_air.PLATFORMS", [Platform.CLIMATE]): + await setup_integration(hass, mock_config_entry) + + status = mock_actron_api.state_manager.get_status.return_value + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: "climate.test_system", ATTR_FAN_MODE: "low"}, + blocking=True, + ) + + status.user_aircon_settings.set_fan_mode.assert_awaited_once_with("LOW") + + +async def test_system_set_fan_mode_api_error( + hass: HomeAssistant, + mock_actron_api: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test API error when setting fan mode for system climate entity.""" + with patch("homeassistant.components.actron_air.PLATFORMS", [Platform.CLIMATE]): + await setup_integration(hass, mock_config_entry) + + status = mock_actron_api.state_manager.get_status.return_value + status.user_aircon_settings.set_fan_mode.side_effect = ActronAirAPIError( + "Test error" + ) + + with pytest.raises(HomeAssistantError, match="Test error"): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: "climate.test_system", ATTR_FAN_MODE: "high"}, + blocking=True, + ) + + +async def test_system_set_hvac_mode( + hass: HomeAssistant, + mock_actron_api: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test setting HVAC mode for system climate entity.""" + with patch("homeassistant.components.actron_air.PLATFORMS", [Platform.CLIMATE]): + await setup_integration(hass, mock_config_entry) + + status = mock_actron_api.state_manager.get_status.return_value + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.test_system", ATTR_HVAC_MODE: HVACMode.COOL}, + blocking=True, + ) + + status.ac_system.set_system_mode.assert_awaited_once_with("COOL") + + +async def test_system_set_hvac_mode_api_error( + hass: HomeAssistant, + mock_actron_api: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test API error when setting HVAC mode for system climate entity.""" + with patch("homeassistant.components.actron_air.PLATFORMS", [Platform.CLIMATE]): + await setup_integration(hass, mock_config_entry) + + status = mock_actron_api.state_manager.get_status.return_value + status.ac_system.set_system_mode.side_effect = ActronAirAPIError("Test error") + + with pytest.raises(HomeAssistantError, match="Test error"): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.test_system", ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + + +async def test_zone_set_temperature( + hass: HomeAssistant, + init_integration_with_zone: None, + mock_zone: MagicMock, +) -> None: + """Test setting temperature for zone climate entity.""" + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: "climate.living_room", ATTR_TEMPERATURE: 23.0}, + blocking=True, + ) + + mock_zone.set_temperature.assert_awaited_once_with(temperature=23.0) + + +async def test_zone_set_temperature_api_error( + hass: HomeAssistant, + init_integration_with_zone: None, + mock_zone: MagicMock, +) -> None: + """Test API error when setting temperature for zone climate entity.""" + mock_zone.set_temperature.side_effect = ActronAirAPIError("Test error") + + with pytest.raises(HomeAssistantError, match="Test error"): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: "climate.living_room", ATTR_TEMPERATURE: 23.0}, + blocking=True, + ) + + +async def test_zone_set_hvac_mode_on( + hass: HomeAssistant, + init_integration_with_zone: None, + mock_zone: MagicMock, +) -> None: + """Test setting HVAC mode to on for zone climate entity.""" + mock_zone.is_active = False + mock_zone.hvac_mode = "OFF" + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.living_room", ATTR_HVAC_MODE: HVACMode.COOL}, + blocking=True, + ) + + mock_zone.enable.assert_awaited_once_with(True) + + +async def test_zone_set_hvac_mode_off( + hass: HomeAssistant, + init_integration_with_zone: None, + mock_zone: MagicMock, +) -> None: + """Test setting HVAC mode to off for zone climate entity.""" + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.living_room", ATTR_HVAC_MODE: HVACMode.OFF}, + blocking=True, + ) + + mock_zone.enable.assert_awaited_once_with(False) + + +async def test_zone_set_hvac_mode_api_error( + hass: HomeAssistant, + init_integration_with_zone: None, + mock_zone: MagicMock, +) -> None: + """Test API error when setting HVAC mode for zone climate entity.""" + mock_zone.enable.side_effect = ActronAirAPIError("Test error") + + with pytest.raises(HomeAssistantError, match="Test error"): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.living_room", ATTR_HVAC_MODE: HVACMode.OFF}, + blocking=True, + ) diff --git a/tests/components/actron_air/test_switch.py b/tests/components/actron_air/test_switch.py index 2464ae8d0a0..ef4b4e2f292 100644 --- a/tests/components/actron_air/test_switch.py +++ b/tests/components/actron_air/test_switch.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock, patch +from actron_neo_api import ActronAirAPIError import pytest from syrupy.assertion import SnapshotAssertion @@ -12,6 +13,7 @@ from homeassistant.components.switch import ( ) from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from . import setup_integration @@ -88,3 +90,37 @@ async def test_turbo_mode_not_supported( entity_id = "switch.test_system_turbo_mode" assert not hass.states.get(entity_id) assert not entity_registry.async_get(entity_id) + + +@pytest.mark.parametrize( + ("entity_id", "method", "service"), + [ + ("switch.test_system_away_mode", "set_away_mode", SERVICE_TURN_ON), + ("switch.test_system_continuous_fan", "set_continuous_mode", SERVICE_TURN_OFF), + ("switch.test_system_quiet_mode", "set_quiet_mode", SERVICE_TURN_ON), + ("switch.test_system_turbo_mode", "set_turbo_mode", SERVICE_TURN_OFF), + ], +) +async def test_switch_api_error( + hass: HomeAssistant, + mock_actron_api: MagicMock, + mock_config_entry: MockConfigEntry, + entity_id: str, + method: str, + service: str, +) -> None: + """Test API error handling when toggling switches.""" + with patch("homeassistant.components.actron_air.PLATFORMS", [Platform.SWITCH]): + await setup_integration(hass, mock_config_entry) + + status = mock_actron_api.state_manager.get_status.return_value + mock_method = getattr(status.user_aircon_settings, method) + mock_method.side_effect = ActronAirAPIError("Test error") + + with pytest.raises(HomeAssistantError, match="Test error"): + await hass.services.async_call( + SWITCH_DOMAIN, + service, + {ATTR_ENTITY_ID: [entity_id]}, + blocking=True, + )