From 7a6897c7578dffd6b67f57747ebd81b67b153e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20J=C3=A4ger?= Date: Wed, 21 Sep 2022 08:20:44 +0200 Subject: [PATCH] Add deconz current hvac operation to thermostate based on "state" (#59989) * deconz - add current hvac operation to thermostate based on "state" * deconz - extend current hvac operation to thermostate based on "state" and "mode" * Add tests for current hvac action * Add boost mode as special case * format using Black * sort imports * Add test for device with mode none and state none * Update homeassistant/components/deconz/climate.py Co-authored-by: Robert Svensson * Fix test_climate.py test_no_mode_no_state * Add test for boost mode Co-authored-by: Robert Svensson --- homeassistant/components/deconz/climate.py | 16 ++ tests/components/deconz/test_climate.py | 201 ++++++++++++++++++++- 2 files changed, 216 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 24cc6c8d5c8..0d13f2639da 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -24,6 +24,7 @@ from homeassistant.components.climate import ( PRESET_ECO, ClimateEntity, ClimateEntityFeature, + HVACAction, HVACMode, ) from homeassistant.config_entries import ConfigEntry @@ -172,6 +173,21 @@ class DeconzThermostat(DeconzDevice[Thermostat], ClimateEntity): mode=HVAC_MODE_TO_DECONZ[hvac_mode], ) + @property + def hvac_action(self) -> str | None: + """Return current hvac operation ie. heat, cool. + + Preset 'BOOST' is interpreted as 'state_on'. + """ + if self._device.mode == ThermostatMode.OFF: + return HVACAction.OFF + + if self._device.state_on or self._device.preset == ThermostatPreset.BOOST: + if self._device.mode == ThermostatMode.COOL: + return HVACAction.COOLING + return HVACAction.HEATING + return HVACAction.IDLE + # Preset control @property diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index d93dd2ebfa1..c621ce02823 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -25,6 +25,7 @@ from homeassistant.components.climate.const import ( PRESET_BOOST, PRESET_COMFORT, PRESET_ECO, + HVACAction, HVACMode, ) from homeassistant.components.deconz.climate import ( @@ -108,6 +109,7 @@ async def test_simple_climate_device(hass, aioclient_mock, mock_deconz_websocket assert climate_thermostat.attributes["temperature"] == 21.0 assert climate_thermostat.attributes["locked"] is True assert hass.states.get("sensor.thermostat_battery").state == "59" + assert climate_thermostat.attributes["hvac_action"] == HVACAction.HEATING # Event signals thermostat configured off @@ -122,6 +124,10 @@ async def test_simple_climate_device(hass, aioclient_mock, mock_deconz_websocket await hass.async_block_till_done() assert hass.states.get("climate.thermostat").state == STATE_OFF + assert ( + hass.states.get("climate.thermostat").attributes["hvac_action"] + == HVACAction.IDLE + ) # Event signals thermostat state on @@ -136,6 +142,10 @@ async def test_simple_climate_device(hass, aioclient_mock, mock_deconz_websocket await hass.async_block_till_done() assert hass.states.get("climate.thermostat").state == HVACMode.HEAT + assert ( + hass.states.get("climate.thermostat").attributes["hvac_action"] + == HVACAction.HEATING + ) # Verify service calls @@ -210,6 +220,10 @@ async def test_climate_device_without_cooling_support( assert hass.states.get("sensor.thermostat_battery").state == "100" assert hass.states.get("climate.presence_sensor") is None assert hass.states.get("climate.clip_thermostat") is None + assert ( + hass.states.get("climate.thermostat").attributes["hvac_action"] + == HVACAction.HEATING + ) # Event signals thermostat configured off @@ -224,6 +238,10 @@ async def test_climate_device_without_cooling_support( await hass.async_block_till_done() assert hass.states.get("climate.thermostat").state == STATE_OFF + assert ( + hass.states.get("climate.thermostat").attributes["hvac_action"] + == HVACAction.OFF + ) # Event signals thermostat state on @@ -239,6 +257,10 @@ async def test_climate_device_without_cooling_support( await hass.async_block_till_done() assert hass.states.get("climate.thermostat").state == HVACMode.HEAT + assert ( + hass.states.get("climate.thermostat").attributes["hvac_action"] + == HVACAction.HEATING + ) # Event signals thermostat state off @@ -253,6 +275,10 @@ async def test_climate_device_without_cooling_support( await hass.async_block_till_done() assert hass.states.get("climate.thermostat").state == STATE_OFF + assert ( + hass.states.get("climate.thermostat").attributes["hvac_action"] + == HVACAction.IDLE + ) # Verify service calls @@ -382,8 +408,11 @@ async def test_climate_device_with_cooling_support( assert climate_thermostat.attributes["current_temperature"] == 23.2 assert climate_thermostat.attributes["temperature"] == 22.2 assert hass.states.get("sensor.zen_01_battery").state == "25" + assert ( + hass.states.get("climate.zen_01").attributes["hvac_action"] == HVACAction.IDLE + ) - # Event signals thermostat state cool + # Event signals thermostat mode cool event_changed_sensor = { "t": "event", @@ -398,6 +427,27 @@ async def test_climate_device_with_cooling_support( assert hass.states.get("climate.zen_01").state == HVACMode.COOL assert hass.states.get("climate.zen_01").attributes["temperature"] == 11.1 + assert ( + hass.states.get("climate.zen_01").attributes["hvac_action"] == HVACAction.IDLE + ) + + # Event signals thermostat state on + + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "state": {"on": True}, + } + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + assert hass.states.get("climate.zen_01").state == HVACMode.COOL + assert ( + hass.states.get("climate.zen_01").attributes["hvac_action"] + == HVACAction.COOLING + ) # Verify service calls @@ -463,6 +513,9 @@ async def test_climate_device_with_fan_support( FAN_ON, FAN_OFF, ] + assert ( + hass.states.get("climate.zen_01").attributes["hvac_action"] == HVACAction.IDLE + ) # Event signals fan mode defaults to off @@ -477,6 +530,9 @@ async def test_climate_device_with_fan_support( await hass.async_block_till_done() assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_OFF + assert ( + hass.states.get("climate.zen_01").attributes["hvac_action"] == HVACAction.IDLE + ) # Event signals unsupported fan mode @@ -492,6 +548,10 @@ async def test_climate_device_with_fan_support( await hass.async_block_till_done() assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_ON + assert ( + hass.states.get("climate.zen_01").attributes["hvac_action"] + == HVACAction.HEATING + ) # Event signals unsupported fan mode @@ -506,6 +566,10 @@ async def test_climate_device_with_fan_support( await hass.async_block_till_done() assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_ON + assert ( + hass.states.get("climate.zen_01").attributes["hvac_action"] + == HVACAction.HEATING + ) # Verify service calls @@ -593,6 +657,9 @@ async def test_climate_device_with_preset(hass, aioclient_mock, mock_deconz_webs DECONZ_PRESET_HOLIDAY, DECONZ_PRESET_MANUAL, ] + assert ( + hass.states.get("climate.zen_01").attributes["hvac_action"] == HVACAction.IDLE + ) # Event signals deCONZ preset @@ -693,6 +760,10 @@ async def test_clip_climate_device(hass, aioclient_mock): assert len(hass.states.async_all()) == 3 assert hass.states.get("climate.clip_thermostat").state == HVACMode.HEAT + assert ( + hass.states.get("climate.clip_thermostat").attributes["hvac_action"] + == HVACAction.HEATING + ) # Disallow clip sensors @@ -713,6 +784,10 @@ async def test_clip_climate_device(hass, aioclient_mock): assert len(hass.states.async_all()) == 3 assert hass.states.get("climate.clip_thermostat").state == HVACMode.HEAT + assert ( + hass.states.get("climate.clip_thermostat").attributes["hvac_action"] + == HVACAction.HEATING + ) async def test_verify_state_update(hass, aioclient_mock, mock_deconz_websocket): @@ -738,6 +813,10 @@ async def test_verify_state_update(hass, aioclient_mock, mock_deconz_websocket): await setup_deconz_integration(hass, aioclient_mock) assert hass.states.get("climate.thermostat").state == HVACMode.AUTO + assert ( + hass.states.get("climate.thermostat").attributes["hvac_action"] + == HVACAction.HEATING + ) event_changed_sensor = { "t": "event", @@ -750,6 +829,10 @@ async def test_verify_state_update(hass, aioclient_mock, mock_deconz_websocket): await hass.async_block_till_done() assert hass.states.get("climate.thermostat").state == HVACMode.AUTO + assert ( + hass.states.get("climate.thermostat").attributes["hvac_action"] + == HVACAction.IDLE + ) async def test_add_new_climate_device(hass, aioclient_mock, mock_deconz_websocket): @@ -784,6 +867,10 @@ async def test_add_new_climate_device(hass, aioclient_mock, mock_deconz_websocke assert len(hass.states.async_all()) == 2 assert hass.states.get("climate.thermostat").state == HVACMode.AUTO assert hass.states.get("sensor.thermostat_battery").state == "100" + assert ( + hass.states.get("climate.thermostat").attributes["hvac_action"] + == HVACAction.HEATING + ) async def test_not_allow_clip_thermostat(hass, aioclient_mock): @@ -806,3 +893,115 @@ async def test_not_allow_clip_thermostat(hass, aioclient_mock): ) assert len(hass.states.async_all()) == 0 + + +async def test_no_mode_no_state(hass, aioclient_mock, mock_deconz_websocket): + """Test that a climate device without mode and state works.""" + data = { + "sensors": { + "0": { + "config": { + "battery": 25, + "heatsetpoint": 2222, + "mode": None, + "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": "none", "on": None, "temperature": 2290}, + "type": "ZHAThermostat", + "uniqueid": "00:24:46:00:00:11:6f:56-01-0201", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + assert len(hass.states.async_all()) == 2 + + climate_thermostat = hass.states.get("climate.zen_01") + + assert climate_thermostat.state is STATE_OFF + assert climate_thermostat.attributes["preset_mode"] is DECONZ_PRESET_AUTO + assert climate_thermostat.attributes["hvac_action"] is HVACAction.IDLE + + # Verify service calls + mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config") + + +async def test_boost_mode(hass, aioclient_mock, mock_deconz_websocket): + """Test that a climate device with boost mode and different state works.""" + data = { + "sensors": { + "0": { + "config": { + "battery": 58, + "heatsetpoint": 2200, + "locked": False, + "mode": "heat", + "offset": -200, + "on": True, + "preset": "manual", + "reachable": True, + "schedule": {}, + "schedule_on": False, + "setvalve": False, + "windowopen_set": False, + }, + "ep": 1, + "etag": "404c15db68c318ebe7832ce5aa3d1e30", + "lastannounced": "2022-08-31T03:00:59Z", + "lastseen": "2022-09-19T11:58Z", + "manufacturername": "_TZE200_b6wax7g0", + "modelid": "TS0601", + "name": "Thermostat", + "state": { + "lastupdated": "2022-09-19T11:58:24.204", + "lowbattery": False, + "on": False, + "temperature": 2200, + "valve": 0, + }, + "type": "ZHAThermostat", + "uniqueid": "84:fd:27:ff:fe:8a:eb:89-01-0201", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + assert len(hass.states.async_all()) == 3 + + climate_thermostat = hass.states.get("climate.thermostat") + + assert climate_thermostat.state == HVACMode.HEAT + + assert climate_thermostat.attributes["preset_mode"] is DECONZ_PRESET_MANUAL + assert climate_thermostat.attributes["hvac_action"] is HVACAction.IDLE + + # Event signals thermostat preset boost and valve 100 (real data) + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "config": {"preset": "boost"}, + "state": {"valve": 100}, + } + + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + climate_thermostat = hass.states.get("climate.thermostat") + assert climate_thermostat.attributes["preset_mode"] is PRESET_BOOST + assert climate_thermostat.attributes["hvac_action"] is HVACAction.HEATING + + # Verify service calls + mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config")