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 <Kane610@users.noreply.github.com>

* Fix test_climate.py test_no_mode_no_state

* Add test for boost mode

Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com>
This commit is contained in:
Michael Jäger 2022-09-21 08:20:44 +02:00 committed by GitHub
parent d7eb277bc8
commit 7a6897c757
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 216 additions and 1 deletions

View File

@ -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

View File

@ -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")