From 317e87d130721cc9a8225223b8530d2b4ff728db Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 28 Aug 2026 22:04:38 +0200 Subject: [PATCH] Follow the requested mode when setting a SwitchBot Cloud AC temperature (#180518) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: balloob <1444314+balloob@users.noreply.github.com> --- .../components/switchbot_cloud/climate.py | 34 ++++--- .../switchbot_cloud/test_climate.py | 96 +++++++++++++++++++ 2 files changed, 119 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/switchbot_cloud/climate.py b/homeassistant/components/switchbot_cloud/climate.py index 0e63dd1bbdc1..4eec9bc3f36f 100644 --- a/homeassistant/components/switchbot_cloud/climate.py +++ b/homeassistant/components/switchbot_cloud/climate.py @@ -15,6 +15,7 @@ from switchbot_api import ( from homeassistant.components import climate as FanState from homeassistant.components.climate import ( + ATTR_HVAC_MODE, ATTR_TEMPERATURE, PRESET_BOOST, PRESET_COMFORT, @@ -149,18 +150,20 @@ class SwitchBotCloudAirConditioner(SwitchBotCloudEntity, ClimateEntity, RestoreE if self.min_temp <= temperature <= self.max_temp: self._attr_target_temperature = temperature - def _get_mode(self, hvac_mode: HVACMode | None) -> int: - new_hvac_mode = hvac_mode or self._attr_hvac_mode + def _get_mode(self, hvac_mode: HVACMode) -> int: + """Return the SwitchBot mode for the command. + + Every command carries a mode, so one that turns the device off carries + the mode it was last running in. + """ _LOGGER.debug( - "Received hvac_mode: %s (Currently set as %s)", + "Resolving mode for hvac_mode: %s (Currently set as %s)", hvac_mode, self._attr_hvac_mode, ) - if new_hvac_mode == HVACMode.OFF: - return _SWITCHBOT_HVAC_MODES.get( - self._attr_hvac_mode, _DEFAULT_SWITCHBOT_HVAC_MODE - ) - return _SWITCHBOT_HVAC_MODES.get(new_hvac_mode, _DEFAULT_SWITCHBOT_HVAC_MODE) + if hvac_mode == HVACMode.OFF: + hvac_mode = self._attr_hvac_mode + return _SWITCHBOT_HVAC_MODES.get(hvac_mode, _DEFAULT_SWITCHBOT_HVAC_MODE) async def _do_send_command( self, @@ -169,11 +172,14 @@ class SwitchBotCloudAirConditioner(SwitchBotCloudEntity, ClimateEntity, RestoreE temperature: float | None = None, ) -> None: new_temperature = temperature or self._attr_target_temperature - new_mode = self._get_mode(hvac_mode) + # A command without a mode of its own follows the mode the entity is + # already in, so it cannot power a device on that is off + new_hvac_mode = hvac_mode or self._attr_hvac_mode + new_mode = self._get_mode(new_hvac_mode) new_fan_speed = _SWITCHBOT_FAN_MODES.get( fan_mode or self._attr_fan_mode, _DEFAULT_SWITCHBOT_FAN_MODE ) - new_power_state = "on" if hvac_mode != HVACMode.OFF else "off" + new_power_state = "on" if new_hvac_mode != HVACMode.OFF else "off" command = f"{int(new_temperature)},{new_mode},{new_fan_speed},{new_power_state}" _LOGGER.debug("Sending command to %s: %s", self._attr_unique_id, command) await self.send_api_command( @@ -200,8 +206,14 @@ class SwitchBotCloudAirConditioner(SwitchBotCloudEntity, ClimateEntity, RestoreE """Set target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return - await self._do_send_command(temperature=temperature) + hvac_mode: HVACMode | None = kwargs.get(ATTR_HVAC_MODE) + if hvac_mode is not None: + self._valid_mode_or_raise("hvac", hvac_mode, self.hvac_modes) + + await self._do_send_command(hvac_mode=hvac_mode, temperature=temperature) self._attr_target_temperature = temperature + if hvac_mode is not None: + self._attr_hvac_mode = hvac_mode self.async_write_ha_state() @override diff --git a/tests/components/switchbot_cloud/test_climate.py b/tests/components/switchbot_cloud/test_climate.py index 8460a94f88de..4357f266d72f 100644 --- a/tests/components/switchbot_cloud/test_climate.py +++ b/tests/components/switchbot_cloud/test_climate.py @@ -21,6 +21,7 @@ from homeassistant.components.climate import ( from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, State +from homeassistant.exceptions import ServiceValidationError from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM from . import configure_integration @@ -132,6 +133,101 @@ async def test_air_conditioner_set_temperature( assert hass.states.get(entity_id).attributes[ATTR_TEMPERATURE] == 25 +@pytest.mark.parametrize( + ("service_data", "expected_command", "expected_state"), + [ + pytest.param( + {ATTR_TEMPERATURE: 27, ATTR_HVAC_MODE: HVACMode.DRY}, + "27,3,1,on", + HVACMode.DRY, + id="with_hvac_mode", + ), + pytest.param( + {ATTR_TEMPERATURE: 27}, + "27,4,1,off", + HVACMode.OFF, + id="without_hvac_mode", + ), + ], +) +async def test_air_conditioner_set_temperature_while_off( + hass: HomeAssistant, + mock_list_devices: AsyncMock, + mock_get_status: AsyncMock, + service_data: dict[str, float | HVACMode], + expected_command: str, + expected_state: HVACMode, +) -> None: + """Test setting the temperature of an air conditioner that is off.""" + mock_list_devices.return_value = [ + Remote( + deviceId="ac-device-id-1", + deviceName="climate-1", + remoteType="Air Conditioner", + hubDeviceId="test-hub-id", + ), + ] + + entity_id = "climate.climate_1" + mock_restore_cache(hass, (State(entity_id, HVACMode.OFF),)) + + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + assert hass.states.get(entity_id).state == HVACMode.OFF + + with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: entity_id, **service_data}, + blocking=True, + ) + mock_send_command.assert_called_once() + assert expected_command in str(mock_send_command.call_args) + + state = hass.states.get(entity_id) + assert state.state == expected_state + assert state.attributes[ATTR_TEMPERATURE] == 27 + + +async def test_air_conditioner_set_temperature_unsupported_hvac_mode( + hass: HomeAssistant, + mock_list_devices: AsyncMock, + mock_get_status: AsyncMock, +) -> None: + """Test setting the temperature with an hvac mode the air conditioner lacks.""" + mock_list_devices.return_value = [ + Remote( + deviceId="ac-device-id-1", + deviceName="climate-1", + remoteType="Air Conditioner", + hubDeviceId="test-hub-id", + ), + ] + + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + entity_id = "climate.climate_1" + + with ( + patch.object(SwitchBotAPI, "send_command") as mock_send_command, + pytest.raises(ServiceValidationError), + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_TEMPERATURE: 27, + ATTR_HVAC_MODE: HVACMode.AUTO, + }, + blocking=True, + ) + + mock_send_command.assert_not_called() + assert hass.states.get(entity_id).state == HVACMode.FAN_ONLY + + async def test_air_conditioner_restore_state( hass: HomeAssistant, mock_list_devices, mock_get_status ) -> None: