From 527ed24bc186d0f1af024e13e6b10be119f1bfb7 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Wed, 30 Sep 2026 11:26:18 +0200 Subject: [PATCH] Handle MELCloud Home climate action errors (#183646) --- .../components/melcloud_home/climate.py | 123 ++++++++++++------ .../components/melcloud_home/test_climate.py | 113 ++++++++++++++++ 2 files changed, 193 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/melcloud_home/climate.py b/homeassistant/components/melcloud_home/climate.py index a7b5636caef8..28d00284ef23 100644 --- a/homeassistant/components/melcloud_home/climate.py +++ b/homeassistant/components/melcloud_home/climate.py @@ -20,7 +20,7 @@ from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .common import async_setup_unit_entities +from .common import async_setup_unit_entities, perform_action from .coordinator import MelCloudHomeConfigEntry, MelCloudHomeCoordinator from .entity import MelCloudHomeATAUnitEntity, MelCloudHomeATWZoneEntity @@ -253,22 +253,29 @@ class ATAClimateEntity(MelCloudHomeATAUnitEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the HVAC mode.""" if hvac_mode == HVACMode.OFF: - await self.coordinator.client.control_ata_unit(self._unit_id, power=False) + await perform_action( + self.coordinator, + self.coordinator.client.control_ata_unit(self._unit_id, power=False), + ) else: - await self.coordinator.client.control_ata_unit( - self._unit_id, - power=True, - operation_mode=ATA_HVAC_MODE_TO_OPERATION[hvac_mode], + await perform_action( + self.coordinator, + self.coordinator.client.control_ata_unit( + self._unit_id, + power=True, + operation_mode=ATA_HVAC_MODE_TO_OPERATION[hvac_mode], + ), ) - await self.coordinator.async_request_refresh() @override async def async_set_temperature(self, **kwargs: Any) -> None: """Set the target temperature.""" - await self.coordinator.client.control_ata_unit( - self._unit_id, set_temperature=kwargs[ATTR_TEMPERATURE] + await perform_action( + self.coordinator, + self.coordinator.client.control_ata_unit( + self._unit_id, set_temperature=kwargs[ATTR_TEMPERATURE] + ), ) - await self.coordinator.async_request_refresh() @property @override @@ -285,39 +292,52 @@ class ATAClimateEntity(MelCloudHomeATAUnitEntity, ClimateEntity): @override async def async_set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None: """Set the horizontal vane direction.""" - await self.coordinator.client.control_ata_unit( - self._unit_id, - vane_horizontal_direction=HA_VANE_HORIZONTAL_TO_ATA[swing_horizontal_mode], + await perform_action( + self.coordinator, + self.coordinator.client.control_ata_unit( + self._unit_id, + vane_horizontal_direction=HA_VANE_HORIZONTAL_TO_ATA[ + swing_horizontal_mode + ], + ), ) - await self.coordinator.async_request_refresh() @override async def async_set_swing_mode(self, swing_mode: str) -> None: """Set the vertical vane direction.""" - await self.coordinator.client.control_ata_unit( - self._unit_id, vane_vertical_direction=HA_VANE_VERTICAL_TO_ATA[swing_mode] + await perform_action( + self.coordinator, + self.coordinator.client.control_ata_unit( + self._unit_id, + vane_vertical_direction=HA_VANE_VERTICAL_TO_ATA[swing_mode], + ), ) - await self.coordinator.async_request_refresh() @override async def async_set_fan_mode(self, fan_mode: str) -> None: """Set the fan mode.""" - await self.coordinator.client.control_ata_unit( - self._unit_id, set_fan_speed=HA_FAN_SPEED_TO_ATA[fan_mode] + await perform_action( + self.coordinator, + self.coordinator.client.control_ata_unit( + self._unit_id, set_fan_speed=HA_FAN_SPEED_TO_ATA[fan_mode] + ), ) - await self.coordinator.async_request_refresh() @override async def async_turn_on(self) -> None: """Turn the unit on.""" - await self.coordinator.client.control_ata_unit(self._unit_id, power=True) - await self.coordinator.async_request_refresh() + await perform_action( + self.coordinator, + self.coordinator.client.control_ata_unit(self._unit_id, power=True), + ) @override async def async_turn_off(self) -> None: """Turn the unit off.""" - await self.coordinator.client.control_ata_unit(self._unit_id, power=False) - await self.coordinator.async_request_refresh() + await perform_action( + self.coordinator, + self.coordinator.client.control_ata_unit(self._unit_id, power=False), + ) class ATWZoneClimateEntity(MelCloudHomeATWZoneEntity, ClimateEntity): @@ -414,45 +434,62 @@ class ATWZoneClimateEntity(MelCloudHomeATWZoneEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the HVAC mode.""" if hvac_mode == HVACMode.OFF: - await self.coordinator.client.control_atw_unit(self._unit_id, power=False) + await perform_action( + self.coordinator, + self.coordinator.client.control_atw_unit(self._unit_id, power=False), + ) else: zone_mode = HVAC_MODE_TO_ATW_ZONE_MODE[hvac_mode] if self.zone_number == 1: - await self.coordinator.client.control_atw_unit( - self._unit_id, - power=True, - operation_mode_zone1=zone_mode, + await perform_action( + self.coordinator, + self.coordinator.client.control_atw_unit( + self._unit_id, + power=True, + operation_mode_zone1=zone_mode, + ), ) else: - await self.coordinator.client.control_atw_unit( - self._unit_id, - power=True, - operation_mode_zone2=zone_mode, + await perform_action( + self.coordinator, + self.coordinator.client.control_atw_unit( + self._unit_id, + power=True, + operation_mode_zone2=zone_mode, + ), ) - await self.coordinator.async_request_refresh() @override async def async_set_temperature(self, **kwargs: Any) -> None: """Set the target temperature.""" temperature = kwargs[ATTR_TEMPERATURE] if self.zone_number == 1: - await self.coordinator.client.control_atw_unit( - self._unit_id, set_temperature_zone1=temperature + await perform_action( + self.coordinator, + self.coordinator.client.control_atw_unit( + self._unit_id, set_temperature_zone1=temperature + ), ) else: - await self.coordinator.client.control_atw_unit( - self._unit_id, set_temperature_zone2=temperature + await perform_action( + self.coordinator, + self.coordinator.client.control_atw_unit( + self._unit_id, set_temperature_zone2=temperature + ), ) - await self.coordinator.async_request_refresh() @override async def async_turn_on(self) -> None: """Turn the zone on.""" - await self.coordinator.client.control_atw_unit(self._unit_id, power=True) - await self.coordinator.async_request_refresh() + await perform_action( + self.coordinator, + self.coordinator.client.control_atw_unit(self._unit_id, power=True), + ) @override async def async_turn_off(self) -> None: """Turn the zone off.""" - await self.coordinator.client.control_atw_unit(self._unit_id, power=False) - await self.coordinator.async_request_refresh() + await perform_action( + self.coordinator, + self.coordinator.client.control_atw_unit(self._unit_id, power=False), + ) diff --git a/tests/components/melcloud_home/test_climate.py b/tests/components/melcloud_home/test_climate.py index e7f81b855b5c..c655409fe7ba 100644 --- a/tests/components/melcloud_home/test_climate.py +++ b/tests/components/melcloud_home/test_climate.py @@ -10,6 +10,9 @@ from aiomelcloudhome import ( ATAVaneHorizontal, ATAVaneVertical, ATWZoneMode, + MelCloudHomeAuthenticationError, + MelCloudHomeConnectionError, + MelCloudHomeTimeoutError, UserContext, ) import pytest @@ -36,6 +39,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from . import setup_integration @@ -521,3 +525,112 @@ async def test_atw_zone_temperature_range( assert (state2 := hass.states.get(ATW_ZONE2_ENTITY_ID)) assert state2.attributes["min_temp"] == 12.0 assert state2.attributes["max_temp"] == 28.0 + + +@pytest.mark.parametrize( + ("entity_id", "method", "service", "service_data"), + [ + pytest.param( + ATA_ENTITY_ID, + "control_ata_unit", + SERVICE_SET_HVAC_MODE, + {ATTR_HVAC_MODE: HVACMode.HEAT}, + id="ata_set_hvac_mode", + ), + pytest.param( + ATA_ENTITY_ID, + "control_ata_unit", + SERVICE_SET_TEMPERATURE, + {ATTR_TEMPERATURE: 23.0}, + id="ata_set_temperature", + ), + pytest.param( + ATA_ENTITY_ID, + "control_ata_unit", + SERVICE_SET_FAN_MODE, + {ATTR_FAN_MODE: "auto"}, + id="ata_set_fan_mode", + ), + pytest.param( + ATA_ENTITY_ID, + "control_ata_unit", + SERVICE_SET_SWING_MODE, + {ATTR_SWING_MODE: "auto"}, + id="ata_set_swing_mode", + ), + pytest.param( + ATA_ENTITY_ID, + "control_ata_unit", + SERVICE_SET_SWING_HORIZONTAL_MODE, + {ATTR_SWING_HORIZONTAL_MODE: "auto"}, + id="ata_set_swing_horizontal_mode", + ), + pytest.param( + ATA_ENTITY_ID, "control_ata_unit", SERVICE_TURN_ON, {}, id="ata_turn_on" + ), + pytest.param( + ATA_ENTITY_ID, "control_ata_unit", SERVICE_TURN_OFF, {}, id="ata_turn_off" + ), + pytest.param( + ATW_ZONE1_ENTITY_ID, + "control_atw_unit", + SERVICE_SET_HVAC_MODE, + {ATTR_HVAC_MODE: HVACMode.HEAT}, + id="atw_set_hvac_mode", + ), + pytest.param( + ATW_ZONE1_ENTITY_ID, + "control_atw_unit", + SERVICE_SET_TEMPERATURE, + {ATTR_TEMPERATURE: 23.0}, + id="atw_set_temperature", + ), + pytest.param( + ATW_ZONE1_ENTITY_ID, + "control_atw_unit", + SERVICE_TURN_ON, + {}, + id="atw_turn_on", + ), + pytest.param( + ATW_ZONE1_ENTITY_ID, + "control_atw_unit", + SERVICE_TURN_OFF, + {}, + id="atw_turn_off", + ), + ], +) +@pytest.mark.parametrize( + ("raise_exception", "translation_key"), + [ + (MelCloudHomeAuthenticationError, "invalid_auth"), + (MelCloudHomeConnectionError, "cannot_connect"), + (MelCloudHomeTimeoutError, "timeout_connect"), + ], +) +async def test_action_exceptions( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_melcloud_client: AsyncMock, + entity_id: str, + method: str, + service: str, + service_data: dict[str, Any], + raise_exception: type[Exception], + translation_key: str, +) -> None: + """Test climate actions raise translated errors on client errors.""" + await setup_integration(hass, mock_config_entry) + + getattr(mock_melcloud_client, method).side_effect = raise_exception + + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + CLIMATE_DOMAIN, + service, + {ATTR_ENTITY_ID: entity_id, **service_data}, + blocking=True, + ) + + assert exc_info.value.translation_key == translation_key