Add zwave_js thermostat fan mode and fan state support (#46793)

* add thermostat fan mode and fan state support

* return when fan mode is not supported

* use get just in case

* validate state key is in states so we dont have to use get

* pylint
This commit is contained in:
Raman Gupta 2021-02-19 18:20:10 -05:00 committed by GitHub
parent 32bec5ea63
commit 4d23ffacd1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 170 additions and 1 deletions

View File

@ -1,5 +1,5 @@
"""Representation of Z-Wave thermostats."""
from typing import Any, Callable, Dict, List, Optional
from typing import Any, Callable, Dict, List, Optional, cast
from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.const import (
@ -33,6 +33,7 @@ from homeassistant.components.climate.const import (
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
PRESET_NONE,
SUPPORT_FAN_MODE,
SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
@ -81,6 +82,8 @@ HVAC_CURRENT_MAP: Dict[int, str] = {
ThermostatOperatingState.THIRD_STAGE_AUX_HEAT: CURRENT_HVAC_HEAT,
}
ATTR_FAN_STATE = "fan_state"
async def async_setup_entry(
hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable
@ -148,6 +151,16 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
add_to_watched_value_ids=True,
check_all_endpoints=True,
)
self._fan_mode = self.get_zwave_value(
THERMOSTAT_MODE_PROPERTY,
CommandClass.THERMOSTAT_FAN_MODE,
add_to_watched_value_ids=True,
)
self._fan_state = self.get_zwave_value(
THERMOSTAT_OPERATING_STATE_PROPERTY,
CommandClass.THERMOSTAT_FAN_STATE,
add_to_watched_value_ids=True,
)
self._set_modes_and_presets()
def _setpoint_value(self, setpoint_type: ThermostatSetpointType) -> ZwaveValue:
@ -275,6 +288,40 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
"""Return a list of available preset modes."""
return list(self._hvac_presets)
@property
def fan_mode(self) -> Optional[str]:
"""Return the fan setting."""
if (
self._fan_mode
and self._fan_mode.value is not None
and str(self._fan_mode.value) in self._fan_mode.metadata.states
):
return cast(str, self._fan_mode.metadata.states[str(self._fan_mode.value)])
return None
@property
def fan_modes(self) -> Optional[List[str]]:
"""Return the list of available fan modes."""
if self._fan_mode and self._fan_mode.metadata.states:
return list(self._fan_mode.metadata.states.values())
return None
@property
def device_state_attributes(self) -> Optional[Dict[str, str]]:
"""Return the optional state attributes."""
if (
self._fan_state
and self._fan_state.value is not None
and str(self._fan_state.value) in self._fan_state.metadata.states
):
return {
ATTR_FAN_STATE: self._fan_state.metadata.states[
str(self._fan_state.value)
]
}
return None
@property
def supported_features(self) -> int:
"""Return the list of supported features."""
@ -283,8 +330,28 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
support |= SUPPORT_TARGET_TEMPERATURE
if len(self._current_mode_setpoint_enums) > 1:
support |= SUPPORT_TARGET_TEMPERATURE_RANGE
if self._fan_mode:
support |= SUPPORT_FAN_MODE
return support
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode."""
if not self._fan_mode:
return
try:
new_state = int(
next(
state
for state, label in self._fan_mode.metadata.states.items()
if label == fan_mode
)
)
except StopIteration:
raise ValueError(f"Received an invalid fan mode: {fan_mode}") from None
await self.info.node.async_set_value(self._fan_mode, new_state)
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
assert self.hass

View File

@ -5,6 +5,7 @@ from zwave_js_server.event import Event
from homeassistant.components.climate.const import (
ATTR_CURRENT_HUMIDITY,
ATTR_CURRENT_TEMPERATURE,
ATTR_FAN_MODE,
ATTR_HVAC_ACTION,
ATTR_HVAC_MODE,
ATTR_HVAC_MODES,
@ -19,10 +20,12 @@ from homeassistant.components.climate.const import (
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
PRESET_NONE,
SERVICE_SET_FAN_MODE,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_PRESET_MODE,
SERVICE_SET_TEMPERATURE,
)
from homeassistant.components.zwave_js.climate import ATTR_FAN_STATE
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE
CLIMATE_RADIO_THERMOSTAT_ENTITY = "climate.z_wave_thermostat"
@ -50,6 +53,8 @@ async def test_thermostat_v2(
assert state.attributes[ATTR_TEMPERATURE] == 22.2
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE
assert state.attributes[ATTR_FAN_MODE] == "Auto low"
assert state.attributes[ATTR_FAN_STATE] == "Idle / off"
# Test setting preset mode
await hass.services.async_call(
@ -329,6 +334,57 @@ async def test_thermostat_v2(
blocking=True,
)
client.async_send_command.reset_mock()
# Test setting fan mode
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{
ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY,
ATTR_FAN_MODE: "Low",
},
blocking=True,
)
assert len(client.async_send_command.call_args_list) == 1
args = client.async_send_command.call_args[0][0]
assert args["command"] == "node.set_value"
assert args["nodeId"] == 13
assert args["valueId"] == {
"endpoint": 1,
"commandClass": 68,
"commandClassName": "Thermostat Fan Mode",
"property": "mode",
"propertyName": "mode",
"ccVersion": 0,
"metadata": {
"type": "number",
"readable": True,
"writeable": True,
"min": 0,
"max": 255,
"states": {"0": "Auto low", "1": "Low"},
"label": "Thermostat fan mode",
},
"value": 0,
}
assert args["value"] == 1
client.async_send_command.reset_mock()
# Test setting invalid fan mode
with pytest.raises(ValueError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{
ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY,
ATTR_FAN_MODE: "fake value",
},
blocking=True,
)
async def test_thermostat_different_endpoints(
hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, integration

View File

@ -594,6 +594,52 @@
"propertyName": "manufacturerData",
"metadata": { "type": "any", "readable": true, "writeable": true }
},
{
"endpoint": 1,
"commandClass": 68,
"commandClassName": "Thermostat Fan Mode",
"property": "mode",
"propertyName": "mode",
"ccVersion": 0,
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"min": 0,
"max": 255,
"states": { "0": "Auto low", "1": "Low" },
"label": "Thermostat fan mode"
},
"value": 0
},
{
"endpoint": 1,
"commandClass": 69,
"commandClassName": "Thermostat Fan State",
"property": "state",
"propertyName": "state",
"ccVersion": 0,
"metadata": {
"type": "number",
"readable": true,
"writeable": true,
"min": 0,
"max": 255,
"states": {
"0": "Idle / off",
"1": "Running / running low",
"2": "Running high",
"3": "Running medium",
"4": "Circulation mode",
"5": "Humidity circulation mode",
"6": "Right - left circulation mode",
"7": "Up - down circulation mode",
"8": "Quiet circulation mode"
},
"label": "Thermostat fan state"
},
"value": 0
},
{
"commandClassName": "Thermostat Operating State",
"commandClass": 66,