From 6e8efe2b679f502a7329e19ff72bcae5b0156539 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 2 Dec 2020 18:08:46 +0100 Subject: [PATCH] Add fan support to deCONZ climate platform (#43721) --- homeassistant/components/deconz/climate.py | 50 ++++++ homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_climate.py | 146 +++++++++++++++++- 5 files changed, 198 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 51830387a27..3e1e1748737 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -5,6 +5,12 @@ from pydeconz.sensor import Thermostat from homeassistant.components.climate import DOMAIN, ClimateEntity from homeassistant.components.climate.const import ( + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + FAN_OFF, + FAN_ON, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, @@ -12,6 +18,7 @@ from homeassistant.components.climate.const import ( PRESET_BOOST, PRESET_COMFORT, PRESET_ECO, + SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -23,6 +30,18 @@ from .const import ATTR_OFFSET, ATTR_VALVE, NEW_SENSOR from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry +DECONZ_FAN_SMART = "smart" + +FAN_MODES = { + DECONZ_FAN_SMART: "smart", + FAN_AUTO: "auto", + FAN_HIGH: "high", + FAN_MEDIUM: "medium", + FAN_LOW: "low", + FAN_ON: "on", + FAN_OFF: "off", +} + HVAC_MODES = { HVAC_MODE_AUTO: "auto", HVAC_MODE_COOL: "cool", @@ -103,6 +122,9 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): self._features = SUPPORT_TARGET_TEMPERATURE + if "fanmode" in device.raw["config"]: + self._features |= SUPPORT_FAN_MODE + if "preset" in device.raw["config"]: self._features |= SUPPORT_PRESET_MODE @@ -111,6 +133,34 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): """Return the list of supported features.""" return self._features + # Fan control + + @property + def fan_mode(self) -> str: + """Return fan operation.""" + for hass_fan_mode, fan_mode in FAN_MODES.items(): + if self._device.fanmode == fan_mode: + return hass_fan_mode + + if self._device.state_on: + return FAN_ON + + return FAN_OFF + + @property + def fan_modes(self) -> list: + """Return the list of available fan operation modes.""" + return list(FAN_MODES) + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + if fan_mode not in FAN_MODES: + raise ValueError(f"Unsupported fan mode {fan_mode}") + + data = {"fanmode": FAN_MODES[fan_mode]} + + await self._device.async_set_config(data) + # HVAC control @property diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index e9b388e29fe..c2846f8c57f 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==75"], + "requirements": ["pydeconz==76"], "ssdp": [ { "manufacturer": "Royal Philips Electronics" diff --git a/requirements_all.txt b/requirements_all.txt index 7172e2bf6fe..08325bc3e7b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1337,7 +1337,7 @@ pydaikin==2.3.1 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==75 +pydeconz==76 # homeassistant.components.delijn pydelijn==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5f02d7953c9..b9f3f083b10 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -673,7 +673,7 @@ pycountry==19.8.18 pydaikin==2.3.1 # homeassistant.components.deconz -pydeconz==75 +pydeconz==76 # homeassistant.components.dexcom pydexcom==0.2.0 diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 0930794afe9..319675cf6f7 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -6,22 +6,33 @@ import pytest from homeassistant.components.climate import ( DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, SERVICE_SET_TEMPERATURE, ) from homeassistant.components.climate.const import ( + ATTR_FAN_MODE, ATTR_HVAC_MODE, ATTR_PRESET_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + FAN_OFF, + FAN_ON, 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.climate import ( + DECONZ_FAN_SMART, + DECONZ_PRESET_MANUAL, +) from homeassistant.components.deconz.const import ( CONF_ALLOW_CLIP_SENSOR, DOMAIN as DECONZ_DOMAIN, @@ -432,6 +443,139 @@ async def test_climate_device_with_cooling_support(hass): ) +async def test_climate_device_with_fan_support(hass): + """Test successful creation of sensor entities.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = { + "0": { + "config": { + "battery": 25, + "coolsetpoint": None, + "fanmode": "auto", + "heatsetpoint": 2222, + "mode": "heat", + "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_thermostat = hass.states.get("climate.zen_01") + assert climate_thermostat.state == HVAC_MODE_HEAT + assert climate_thermostat.attributes["fan_mode"] == FAN_AUTO + assert climate_thermostat.attributes["fan_modes"] == [ + DECONZ_FAN_SMART, + FAN_AUTO, + FAN_HIGH, + FAN_MEDIUM, + FAN_LOW, + FAN_ON, + FAN_OFF, + ] + + # Event signals fan mode defaults to off + + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "config": {"fanmode": "unsupported"}, + } + gateway.api.event_handler(state_changed_event) + await hass.async_block_till_done() + + assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_OFF + + # Event signals unsupported fan mode + + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "config": {"fanmode": "unsupported"}, + "state": {"on": True}, + } + gateway.api.event_handler(state_changed_event) + await hass.async_block_till_done() + + assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_ON + + # Event signals unsupported fan mode + + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "config": {"fanmode": "unsupported"}, + } + gateway.api.event_handler(state_changed_event) + await hass.async_block_till_done() + + assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_ON + + # Verify service calls + + thermostat_device = gateway.api.sensors["0"] + + # Service set fan mode to off + + with patch.object(thermostat_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: "climate.zen_01", ATTR_FAN_MODE: FAN_OFF}, + blocking=True, + ) + set_callback.assert_called_with( + "put", "/sensors/0/config", json={"fanmode": "off"} + ) + + # Service set fan mode to custom deCONZ mode smart + + with patch.object(thermostat_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: "climate.zen_01", ATTR_FAN_MODE: DECONZ_FAN_SMART}, + blocking=True, + ) + set_callback.assert_called_with( + "put", "/sensors/0/config", json={"fanmode": "smart"} + ) + + # Service set fan mode 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_FAN_MODE, + {ATTR_ENTITY_ID: "climate.zen_01", ATTR_FAN_MODE: "unsupported"}, + blocking=True, + ) + + async def test_climate_device_with_preset(hass): """Test successful creation of sensor entities.""" data = deepcopy(DECONZ_WEB_REQUEST)