diff --git a/.coveragerc b/.coveragerc index 9e1ec415006..01c0a657f31 100644 --- a/.coveragerc +++ b/.coveragerc @@ -580,7 +580,6 @@ omit = homeassistant/components/nest/camera_legacy.py homeassistant/components/nest/climate.py homeassistant/components/nest/climate_legacy.py - homeassistant/components/nest/climate_sdm.py homeassistant/components/nest/local_auth.py homeassistant/components/nest/sensor.py homeassistant/components/nest/sensor_legacy.py diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index e56d35c1dff..fbaeb502b42 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -186,8 +186,6 @@ class ThermostatEntity(ClimateEntity): @property def _target_temperature_trait(self): """Return the correct trait with a target temp depending on mode.""" - if not self.hvac_mode: - return None if self.preset_mode == PRESET_ECO: if ThermostatEcoTrait.NAME in self._device.traits: return self._device.traits[ThermostatEcoTrait.NAME] @@ -225,8 +223,6 @@ class ThermostatEntity(ClimateEntity): @property def hvac_action(self): """Return the current HVAC action (heating, cooling).""" - if ThermostatHvacTrait.NAME not in self._device.traits: - return None trait = self._device.traits[ThermostatHvacTrait.NAME] if trait.status in THERMOSTAT_HVAC_STATUS_MAP: return THERMOSTAT_HVAC_STATUS_MAP[trait.status] @@ -262,9 +258,10 @@ class ThermostatEntity(ClimateEntity): @property def fan_modes(self): """Return the list of available fan modes.""" + modes = [] if FanTrait.NAME in self._device.traits: - return list(FAN_INV_MODE_MAP) - return [] + modes = list(FAN_INV_MODE_MAP) + return modes @property def supported_features(self): @@ -290,12 +287,8 @@ class ThermostatEntity(ClimateEntity): async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" if hvac_mode not in self.hvac_modes: - return - if hvac_mode not in THERMOSTAT_INV_MODE_MAP: - return + raise ValueError(f"Unsupported hvac_mode '{hvac_mode}'") api_mode = THERMOSTAT_INV_MODE_MAP[hvac_mode] - if ThermostatModeTrait.NAME not in self._device.traits: - return trait = self._device.traits[ThermostatModeTrait.NAME] await trait.set_mode(api_mode) @@ -318,17 +311,13 @@ class ThermostatEntity(ClimateEntity): async def async_set_preset_mode(self, preset_mode): """Set new target preset mode.""" if preset_mode not in self.preset_modes: - return - if ThermostatEcoTrait.NAME not in self._device.traits: - return + raise ValueError(f"Unsupported preset_mode '{preset_mode}'") trait = self._device.traits[ThermostatEcoTrait.NAME] await trait.set_mode(PRESET_INV_MODE_MAP[preset_mode]) async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" if fan_mode not in self.fan_modes: - return - if FanTrait.NAME not in self._device.traits: - return + raise ValueError(f"Unsupported fan__mode '{fan_mode}'") trait = self._device.traits[FanTrait.NAME] await trait.set_timer(FAN_INV_MODE_MAP[fan_mode]) diff --git a/tests/components/nest/climate_sdm_test.py b/tests/components/nest/climate_sdm_test.py index bf6716ec966..886b67f8e2a 100644 --- a/tests/components/nest/climate_sdm_test.py +++ b/tests/components/nest/climate_sdm_test.py @@ -7,6 +7,7 @@ pubsub subscriber. from google_nest_sdm.device import Device from google_nest_sdm.event import EventMessage +import pytest from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, @@ -21,14 +22,17 @@ from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_OFF, + FAN_LOW, FAN_OFF, FAN_ON, HVAC_MODE_COOL, + HVAC_MODE_DRY, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_ECO, PRESET_NONE, + PRESET_SLEEP, ) from homeassistant.const import ATTR_TEMPERATURE @@ -450,6 +454,34 @@ async def test_thermostat_set_hvac_mode(hass, auth): assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT +async def test_thermostat_invalid_hvac_mode(hass, auth): + """Test setting an hvac_mode that is not supported.""" + await setup_climate( + hass, + { + "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_HVAC_ACTION] == CURRENT_HVAC_OFF + + with pytest.raises(ValueError): + await common.async_set_hvac_mode(hass, HVAC_MODE_DRY) + await hass.async_block_till_done() + + assert thermostat.state == HVAC_MODE_OFF + assert auth.method is None # No communication with API + + async def test_thermostat_set_eco_preset(hass, auth): """Test a thermostat put into eco mode.""" subscriber = await setup_climate( @@ -782,6 +814,53 @@ async def test_thermostat_fan_empty(hass): assert ATTR_FAN_MODE not in thermostat.attributes assert ATTR_FAN_MODES not in thermostat.attributes + # Ignores set_fan_mode since it is lacking SUPPORT_FAN_MODE + await common.async_set_fan_mode(hass, FAN_ON) + await hass.async_block_till_done() + + assert ATTR_FAN_MODE not in thermostat.attributes + assert ATTR_FAN_MODES not in thermostat.attributes + + +async def test_thermostat_invalid_fan_mode(hass): + """Test setting a fan mode that is not supported.""" + 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": "OFF", + }, + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 16.2, + }, + }, + ) + + 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_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_OFF, + } + assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON + assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + + with pytest.raises(ValueError): + await common.async_set_fan_mode(hass, FAN_LOW) + await hass.async_block_till_done() + async def test_thermostat_target_temp(hass, auth): """Test a thermostat changing hvac modes and affected on target temps.""" @@ -843,3 +922,208 @@ async def test_thermostat_target_temp(hass, auth): assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] == 22.0 assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] == 28.0 assert thermostat.attributes[ATTR_TEMPERATURE] is None + + +async def test_thermostat_missing_mode_traits(hass): + """Test a thermostat missing many thermostat traits in api response.""" + await setup_climate( + hass, + { + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + }, + ) + + 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_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] is None + assert set(thermostat.attributes[ATTR_HVAC_MODES]) == set() + assert ATTR_TEMPERATURE not in thermostat.attributes + assert ATTR_TARGET_TEMP_LOW not in thermostat.attributes + assert ATTR_TARGET_TEMP_HIGH not in thermostat.attributes + assert ATTR_PRESET_MODE not in thermostat.attributes + assert ATTR_PRESET_MODES not in thermostat.attributes + assert ATTR_FAN_MODE not in thermostat.attributes + assert ATTR_FAN_MODES not in thermostat.attributes + + await common.async_set_temperature(hass, temperature=24.0) + await hass.async_block_till_done() + assert ATTR_TEMPERATURE not in thermostat.attributes + + await common.async_set_preset_mode(hass, PRESET_ECO) + await hass.async_block_till_done() + assert ATTR_PRESET_MODE not in thermostat.attributes + + +async def test_thermostat_missing_temperature_trait(hass): + """Test a thermostat missing many thermostat traits in api response.""" + await setup_climate( + hass, + { + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "HEAT", + }, + }, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_HEAT + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] is None + assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + } + assert thermostat.attributes[ATTR_TEMPERATURE] is None + assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] is None + assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] is None + assert ATTR_PRESET_MODE not in thermostat.attributes + assert ATTR_PRESET_MODES not in thermostat.attributes + assert ATTR_FAN_MODE not in thermostat.attributes + assert ATTR_FAN_MODES not in thermostat.attributes + + await common.async_set_temperature(hass, temperature=24.0) + await hass.async_block_till_done() + assert thermostat.attributes[ATTR_TEMPERATURE] is None + + +async def test_thermostat_unexpected_hvac_status(hass): + """Test a thermostat missing many thermostat traits in api response.""" + await setup_climate( + hass, + { + "sdm.devices.traits.ThermostatHvac": {"status": "UNEXPECTED"}, + }, + ) + + 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 ATTR_HVAC_ACTION not in thermostat.attributes + assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] is None + assert set(thermostat.attributes[ATTR_HVAC_MODES]) == set() + assert ATTR_TEMPERATURE not in thermostat.attributes + assert ATTR_TARGET_TEMP_LOW not in thermostat.attributes + assert ATTR_TARGET_TEMP_HIGH not in thermostat.attributes + assert ATTR_PRESET_MODE not in thermostat.attributes + assert ATTR_PRESET_MODES not in thermostat.attributes + assert ATTR_FAN_MODE not in thermostat.attributes + assert ATTR_FAN_MODES not in thermostat.attributes + + with pytest.raises(ValueError): + await common.async_set_hvac_mode(hass, HVAC_MODE_DRY) + await hass.async_block_till_done() + assert thermostat.state == HVAC_MODE_OFF + + +async def test_thermostat_missing_set_point(hass): + """Test a thermostat missing many thermostat traits in api response.""" + await setup_climate( + hass, + { + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "HEATCOOL", + }, + }, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_HEAT_COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] is None + assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + } + assert thermostat.attributes[ATTR_TEMPERATURE] is None + assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] is None + assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] is None + assert ATTR_PRESET_MODE not in thermostat.attributes + assert ATTR_PRESET_MODES not in thermostat.attributes + assert ATTR_FAN_MODE not in thermostat.attributes + assert ATTR_FAN_MODES not in thermostat.attributes + + +async def test_thermostat_unexepected_hvac_mode(hass): + """Test a thermostat missing many thermostat traits in api response.""" + await setup_climate( + hass, + { + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF", "UNEXPECTED"], + "mode": "UNEXPECTED", + }, + }, + ) + + 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_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] is None + assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + } + assert thermostat.attributes[ATTR_TEMPERATURE] is None + assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] is None + assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] is None + assert ATTR_PRESET_MODE not in thermostat.attributes + assert ATTR_PRESET_MODES not in thermostat.attributes + assert ATTR_FAN_MODE not in thermostat.attributes + assert ATTR_FAN_MODES not in thermostat.attributes + + +async def test_thermostat_invalid_set_preset_mode(hass, auth): + """Test a thermostat set with an invalid preset mode.""" + await setup_climate( + hass, + { + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + "sdm.devices.traits.ThermostatEco": { + "availableModes": ["MANUAL_ECO", "OFF"], + "mode": "OFF", + "heatCelsius": 15.0, + "coolCelsius": 28.0, + }, + }, + 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_PRESET_MODE] == PRESET_NONE + assert thermostat.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_NONE] + + # Set preset mode that is invalid + with pytest.raises(ValueError): + await common.async_set_preset_mode(hass, PRESET_SLEEP) + await hass.async_block_till_done() + + # No RPC sent + assert auth.method is None + + # Preset is unchanged + assert thermostat.attributes[ATTR_PRESET_MODE] == PRESET_NONE + assert thermostat.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_NONE]