diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 278d4144b6f..51830387a27 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -1,4 +1,6 @@ """Support for deCONZ climate devices.""" +from typing import Optional + from pydeconz.sensor import Thermostat from homeassistant.components.climate import DOMAIN, ClimateEntity @@ -7,6 +9,10 @@ from homeassistant.components.climate.const import ( HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, + PRESET_BOOST, + PRESET_COMFORT, + PRESET_ECO, + SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS @@ -24,6 +30,21 @@ HVAC_MODES = { HVAC_MODE_OFF: "off", } +DECONZ_PRESET_AUTO = "auto" +DECONZ_PRESET_COMPLEX = "complex" +DECONZ_PRESET_HOLIDAY = "holiday" +DECONZ_PRESET_MANUAL = "manual" + +PRESET_MODES = { + DECONZ_PRESET_AUTO: "auto", + PRESET_BOOST: "boost", + PRESET_COMFORT: "comfort", + DECONZ_PRESET_COMPLEX: "complex", + PRESET_ECO: "eco", + DECONZ_PRESET_HOLIDAY: "holiday", + DECONZ_PRESET_MANUAL: "manual", +} + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ climate devices. @@ -82,6 +103,9 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): self._features = SUPPORT_TARGET_TEMPERATURE + if "preset" in device.raw["config"]: + self._features |= SUPPORT_PRESET_MODE + @property def supported_features(self): """Return the list of supported features.""" @@ -120,6 +144,31 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): await self._device.async_set_config(data) + # Preset control + + @property + def preset_mode(self) -> Optional[str]: + """Return preset mode.""" + for hass_preset_mode, preset_mode in PRESET_MODES.items(): + if self._device.preset == preset_mode: + return hass_preset_mode + + return None + + @property + def preset_modes(self) -> list: + """Return the list of available preset modes.""" + return list(PRESET_MODES) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + if preset_mode not in PRESET_MODES: + raise ValueError(f"Unsupported preset mode {preset_mode}") + + data = {"preset": PRESET_MODES[preset_mode]} + + await self._device.async_set_config(data) + # Temperature control @property diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index cea660b855b..0930794afe9 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -7,17 +7,21 @@ import pytest from homeassistant.components.climate import ( DOMAIN as CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, SERVICE_SET_TEMPERATURE, ) from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, + ATTR_PRESET_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, + PRESET_COMFORT, ) +from homeassistant.components.deconz.climate import DECONZ_PRESET_MANUAL from homeassistant.components.deconz.const import ( CONF_ALLOW_CLIP_SENSOR, DOMAIN as DECONZ_DOMAIN, @@ -428,6 +432,133 @@ async def test_climate_device_with_cooling_support(hass): ) +async def test_climate_device_with_preset(hass): + """Test successful creation of sensor entities.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = { + "0": { + "config": { + "battery": 25, + "coolsetpoint": None, + "fanmode": None, + "heatsetpoint": 2222, + "mode": "heat", + "preset": "auto", + "offset": 0, + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "074549903686a77a12ef0f06c499b1ef", + "lastseen": "2020-11-27T13:45Z", + "manufacturername": "Zen Within", + "modelid": "Zen-01", + "name": "Zen-01", + "state": { + "lastupdated": "2020-11-27T13:42:40.863", + "on": False, + "temperature": 2320, + }, + "type": "ZHAThermostat", + "uniqueid": "00:24:46:00:00:11:6f:56-01-0201", + } + } + config_entry = await setup_deconz_integration(hass, get_state_response=data) + gateway = get_gateway_from_config_entry(hass, config_entry) + + assert len(hass.states.async_all()) == 2 + + climate_zen_01 = hass.states.get("climate.zen_01") + assert climate_zen_01.state == HVAC_MODE_HEAT + assert climate_zen_01.attributes["current_temperature"] == 23.2 + assert climate_zen_01.attributes["temperature"] == 22.2 + assert climate_zen_01.attributes["preset_mode"] == "auto" + assert climate_zen_01.attributes["preset_modes"] == [ + "auto", + "boost", + "comfort", + "complex", + "eco", + "holiday", + "manual", + ] + + # Event signals deCONZ preset + + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "config": {"preset": "manual"}, + } + gateway.api.event_handler(state_changed_event) + await hass.async_block_till_done() + + assert ( + hass.states.get("climate.zen_01").attributes["preset_mode"] + == DECONZ_PRESET_MANUAL + ) + + # Event signals unknown preset + + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "config": {"preset": "unsupported"}, + } + gateway.api.event_handler(state_changed_event) + await hass.async_block_till_done() + + assert hass.states.get("climate.zen_01").attributes["preset_mode"] is None + + # Verify service calls + + thermostat_device = gateway.api.sensors["0"] + + # Service set preset to HASS preset + + with patch.object(thermostat_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: "climate.zen_01", ATTR_PRESET_MODE: PRESET_COMFORT}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with( + "put", "/sensors/0/config", json={"preset": "comfort"} + ) + + # Service set preset to custom deCONZ preset + + with patch.object(thermostat_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: "climate.zen_01", ATTR_PRESET_MODE: DECONZ_PRESET_MANUAL}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with( + "put", "/sensors/0/config", json={"preset": "manual"} + ) + + # Service set preset to unsupported value + + with patch.object( + thermostat_device, "_request", return_value=True + ) as set_callback, pytest.raises(ValueError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: "climate.zen_01", ATTR_PRESET_MODE: "unsupported"}, + blocking=True, + ) + + async def test_clip_climate_device(hass): """Test successful creation of sensor entities.""" data = deepcopy(DECONZ_WEB_REQUEST)