diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 08cb0161bd9..6413b2e0dfe 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -23,6 +23,7 @@ from homeassistant.components.climate.const import ( FAN_ON, HVAC_MODE_AUTO, HVAC_MODE_COOL, + HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, @@ -188,11 +189,14 @@ class ThermostatEntity(ClimateEntity): @property def hvac_mode(self): """Return the current operation (e.g. heat, cool, idle).""" + hvac_mode = HVAC_MODE_OFF if ThermostatModeTrait.NAME in self._device.traits: trait = self._device.traits[ThermostatModeTrait.NAME] if trait.mode in THERMOSTAT_MODE_MAP: - return THERMOSTAT_MODE_MAP[trait.mode] - return HVAC_MODE_OFF + hvac_mode = THERMOSTAT_MODE_MAP[trait.mode] + if hvac_mode == HVAC_MODE_OFF and self.fan_mode == FAN_ON: + hvac_mode = HVAC_MODE_FAN_ONLY + return hvac_mode @property def hvac_modes(self): @@ -201,6 +205,8 @@ class ThermostatEntity(ClimateEntity): for mode in self._get_device_hvac_modes: if mode in THERMOSTAT_MODE_MAP: supported_modes.append(THERMOSTAT_MODE_MAP[mode]) + if self.supported_features & SUPPORT_FAN_MODE: + supported_modes.append(HVAC_MODE_FAN_ONLY) return supported_modes @property @@ -280,6 +286,10 @@ class ThermostatEntity(ClimateEntity): """Set new target hvac mode.""" if hvac_mode not in self.hvac_modes: raise ValueError(f"Unsupported hvac_mode '{hvac_mode}'") + if hvac_mode == HVAC_MODE_FAN_ONLY: + # Turn the fan on but also turn off the hvac if it is on + await self.async_set_fan_mode(FAN_ON) + hvac_mode = HVAC_MODE_OFF api_mode = THERMOSTAT_INV_MODE_MAP[hvac_mode] trait = self._device.traits[ThermostatModeTrait.NAME] await trait.set_mode(api_mode) diff --git a/tests/components/nest/climate_sdm_test.py b/tests/components/nest/climate_sdm_test.py index 886b67f8e2a..43a422e223e 100644 --- a/tests/components/nest/climate_sdm_test.py +++ b/tests/components/nest/climate_sdm_test.py @@ -27,6 +27,7 @@ from homeassistant.components.climate.const import ( FAN_ON, HVAC_MODE_COOL, HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, @@ -699,6 +700,7 @@ async def test_thermostat_fan_off(hass): HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_OFF @@ -730,13 +732,49 @@ async def test_thermostat_fan_on(hass): assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF + assert thermostat.state == HVAC_MODE_FAN_ONLY assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_OFF, + } + assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON + assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + + +async def test_thermostat_cool_with_fan(hass): + """Test a thermostat cooling while the fan is on.""" + await setup_climate( + hass, + { + "sdm.devices.traits.Fan": { + "timerMode": "ON", + "timerTimeout": "2019-05-10T03:22:54Z", + }, + "sdm.devices.traits.ThermostatHvac": { + "status": "OFF", + }, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "COOL", + }, + }, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON @@ -766,7 +804,7 @@ async def test_thermostat_set_fan(hass, auth): assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF + assert thermostat.state == HVAC_MODE_FAN_ONLY assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] @@ -845,13 +883,14 @@ async def test_thermostat_invalid_fan_mode(hass): assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF + assert thermostat.state == HVAC_MODE_FAN_ONLY assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON @@ -862,6 +901,54 @@ async def test_thermostat_invalid_fan_mode(hass): await hass.async_block_till_done() +async def test_thermostat_set_hvac_fan_only(hass, auth): + """Test a thermostat enabling the fan via hvac_mode.""" + await setup_climate( + hass, + { + "sdm.devices.traits.Fan": { + "timerMode": "OFF", + "timerTimeout": "2019-05-10T03:22:54Z", + }, + "sdm.devices.traits.ThermostatHvac": { + "status": "OFF", + }, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "OFF", + }, + }, + auth=auth, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_OFF + assert thermostat.attributes[ATTR_FAN_MODE] == FAN_OFF + assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + + await common.async_set_hvac_mode(hass, HVAC_MODE_FAN_ONLY) + await hass.async_block_till_done() + + assert len(auth.captured_requests) == 2 + + (method, url, json) = auth.captured_requests.pop(0) + assert method == "post" + assert url == "some-device-id:executeCommand" + assert json == { + "command": "sdm.devices.commands.Fan.SetTimer", + "params": {"timerMode": "ON"}, + } + (method, url, json) = auth.captured_requests.pop(0) + assert method == "post" + assert url == "some-device-id:executeCommand" + assert json == { + "command": "sdm.devices.commands.ThermostatMode.SetMode", + "params": {"mode": "OFF"}, + } + + async def test_thermostat_target_temp(hass, auth): """Test a thermostat changing hvac modes and affected on target temps.""" subscriber = await setup_climate( diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py index 7e183ab9c82..4ab780f57e6 100644 --- a/tests/components/nest/conftest.py +++ b/tests/components/nest/conftest.py @@ -14,19 +14,18 @@ class FakeAuth(AbstractAuth): from the API. """ - # Tests can set fake responses here. - responses = [] - # The last request is recorded here. - method = None - url = None - json = None - - # Set up by fixture - client = None - def __init__(self): """Initialize FakeAuth.""" super().__init__(None, None) + # Tests can set fake responses here. + self.responses = [] + # The last request is recorded here. + self.method = None + self.url = None + self.json = None + self.captured_requests = [] + # Set up by fixture + self.client = None async def async_get_access_token(self) -> str: """Return a valid access token.""" @@ -37,6 +36,7 @@ class FakeAuth(AbstractAuth): self.method = method self.url = url self.json = json + self.captured_requests.append((method, url, json)) return await self.client.get("/") async def response_handler(self, request):