mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
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:
parent
32bec5ea63
commit
4d23ffacd1
@ -1,5 +1,5 @@
|
|||||||
"""Representation of Z-Wave thermostats."""
|
"""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.client import Client as ZwaveClient
|
||||||
from zwave_js_server.const import (
|
from zwave_js_server.const import (
|
||||||
@ -33,6 +33,7 @@ from homeassistant.components.climate.const import (
|
|||||||
HVAC_MODE_HEAT_COOL,
|
HVAC_MODE_HEAT_COOL,
|
||||||
HVAC_MODE_OFF,
|
HVAC_MODE_OFF,
|
||||||
PRESET_NONE,
|
PRESET_NONE,
|
||||||
|
SUPPORT_FAN_MODE,
|
||||||
SUPPORT_PRESET_MODE,
|
SUPPORT_PRESET_MODE,
|
||||||
SUPPORT_TARGET_TEMPERATURE,
|
SUPPORT_TARGET_TEMPERATURE,
|
||||||
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||||
@ -81,6 +82,8 @@ HVAC_CURRENT_MAP: Dict[int, str] = {
|
|||||||
ThermostatOperatingState.THIRD_STAGE_AUX_HEAT: CURRENT_HVAC_HEAT,
|
ThermostatOperatingState.THIRD_STAGE_AUX_HEAT: CURRENT_HVAC_HEAT,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ATTR_FAN_STATE = "fan_state"
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable
|
hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable
|
||||||
@ -148,6 +151,16 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
|||||||
add_to_watched_value_ids=True,
|
add_to_watched_value_ids=True,
|
||||||
check_all_endpoints=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()
|
self._set_modes_and_presets()
|
||||||
|
|
||||||
def _setpoint_value(self, setpoint_type: ThermostatSetpointType) -> ZwaveValue:
|
def _setpoint_value(self, setpoint_type: ThermostatSetpointType) -> ZwaveValue:
|
||||||
@ -275,6 +288,40 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
|||||||
"""Return a list of available preset modes."""
|
"""Return a list of available preset modes."""
|
||||||
return list(self._hvac_presets)
|
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
|
@property
|
||||||
def supported_features(self) -> int:
|
def supported_features(self) -> int:
|
||||||
"""Return the list of supported features."""
|
"""Return the list of supported features."""
|
||||||
@ -283,8 +330,28 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
|||||||
support |= SUPPORT_TARGET_TEMPERATURE
|
support |= SUPPORT_TARGET_TEMPERATURE
|
||||||
if len(self._current_mode_setpoint_enums) > 1:
|
if len(self._current_mode_setpoint_enums) > 1:
|
||||||
support |= SUPPORT_TARGET_TEMPERATURE_RANGE
|
support |= SUPPORT_TARGET_TEMPERATURE_RANGE
|
||||||
|
if self._fan_mode:
|
||||||
|
support |= SUPPORT_FAN_MODE
|
||||||
return support
|
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:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
assert self.hass
|
assert self.hass
|
||||||
|
@ -5,6 +5,7 @@ from zwave_js_server.event import Event
|
|||||||
from homeassistant.components.climate.const import (
|
from homeassistant.components.climate.const import (
|
||||||
ATTR_CURRENT_HUMIDITY,
|
ATTR_CURRENT_HUMIDITY,
|
||||||
ATTR_CURRENT_TEMPERATURE,
|
ATTR_CURRENT_TEMPERATURE,
|
||||||
|
ATTR_FAN_MODE,
|
||||||
ATTR_HVAC_ACTION,
|
ATTR_HVAC_ACTION,
|
||||||
ATTR_HVAC_MODE,
|
ATTR_HVAC_MODE,
|
||||||
ATTR_HVAC_MODES,
|
ATTR_HVAC_MODES,
|
||||||
@ -19,10 +20,12 @@ from homeassistant.components.climate.const import (
|
|||||||
HVAC_MODE_HEAT_COOL,
|
HVAC_MODE_HEAT_COOL,
|
||||||
HVAC_MODE_OFF,
|
HVAC_MODE_OFF,
|
||||||
PRESET_NONE,
|
PRESET_NONE,
|
||||||
|
SERVICE_SET_FAN_MODE,
|
||||||
SERVICE_SET_HVAC_MODE,
|
SERVICE_SET_HVAC_MODE,
|
||||||
SERVICE_SET_PRESET_MODE,
|
SERVICE_SET_PRESET_MODE,
|
||||||
SERVICE_SET_TEMPERATURE,
|
SERVICE_SET_TEMPERATURE,
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.zwave_js.climate import ATTR_FAN_STATE
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE
|
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE
|
||||||
|
|
||||||
CLIMATE_RADIO_THERMOSTAT_ENTITY = "climate.z_wave_thermostat"
|
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_TEMPERATURE] == 22.2
|
||||||
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
|
assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE
|
||||||
assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE
|
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
|
# Test setting preset mode
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
@ -329,6 +334,57 @@ async def test_thermostat_v2(
|
|||||||
blocking=True,
|
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(
|
async def test_thermostat_different_endpoints(
|
||||||
hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, integration
|
hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, integration
|
||||||
|
@ -594,6 +594,52 @@
|
|||||||
"propertyName": "manufacturerData",
|
"propertyName": "manufacturerData",
|
||||||
"metadata": { "type": "any", "readable": true, "writeable": true }
|
"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",
|
"commandClassName": "Thermostat Operating State",
|
||||||
"commandClass": 66,
|
"commandClass": 66,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user