Improve climate turn_on/turn_off services for zwave_js (#109187)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
b60f931e6d
commit
c1d61b9748
2 changed files with 273 additions and 5 deletions
|
@ -139,10 +139,19 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
|||
self._hvac_modes: dict[HVACMode, int | None] = {}
|
||||
self._hvac_presets: dict[str, int | None] = {}
|
||||
self._unit_value: ZwaveValue | None = None
|
||||
self._last_hvac_mode_id_before_off: int | None = None
|
||||
|
||||
self._current_mode = self.get_zwave_value(
|
||||
THERMOSTAT_MODE_PROPERTY, command_class=CommandClass.THERMOSTAT_MODE
|
||||
)
|
||||
self._supports_resume: bool = bool(
|
||||
self._current_mode
|
||||
and (
|
||||
str(ThermostatMode.RESUME_ON.value)
|
||||
in self._current_mode.metadata.states
|
||||
)
|
||||
)
|
||||
|
||||
self._setpoint_values: dict[ThermostatSetpointType, ZwaveValue | None] = {}
|
||||
for enum in ThermostatSetpointType:
|
||||
self._setpoint_values[enum] = self.get_zwave_value(
|
||||
|
@ -196,13 +205,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
|||
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
|
||||
if HVACMode.OFF in self._hvac_modes:
|
||||
self._attr_supported_features |= ClimateEntityFeature.TURN_OFF
|
||||
|
||||
# We can only support turn on if we are able to turn the device off,
|
||||
# otherwise the device can be considered always on
|
||||
if len(self._hvac_modes) == 2 or any(
|
||||
mode in self._hvac_modes
|
||||
for mode in (HVACMode.HEAT, HVACMode.COOL, HVACMode.HEAT_COOL)
|
||||
):
|
||||
if len(self._hvac_modes) > 1:
|
||||
self._attr_supported_features |= ClimateEntityFeature.TURN_ON
|
||||
# If any setpoint value exists, we can assume temperature
|
||||
# can be set
|
||||
|
@ -496,8 +501,54 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
|||
# Thermostat(valve) has no support for setting a mode, so we make it a no-op
|
||||
return
|
||||
|
||||
# When turning the HVAC off from an on state, store the last HVAC mode ID so we
|
||||
# can set it again when turning the device back on.
|
||||
if hvac_mode == HVACMode.OFF and self._current_mode.value != ThermostatMode.OFF:
|
||||
self._last_hvac_mode_id_before_off = self._current_mode.value
|
||||
await self._async_set_value(self._current_mode, hvac_mode_id)
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the entity off."""
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the entity on."""
|
||||
# If current mode is not off, do nothing
|
||||
if self.hvac_mode != HVACMode.OFF:
|
||||
return
|
||||
|
||||
# We can safely assert here because this function can only be called if the
|
||||
# device can be turned off and on which would require the device to have the
|
||||
# current mode Z-Wave Value
|
||||
assert self._current_mode
|
||||
|
||||
# If the device supports resume, use resume to get to the right mode
|
||||
if self._supports_resume:
|
||||
await self._async_set_value(self._current_mode, ThermostatMode.RESUME_ON)
|
||||
return
|
||||
|
||||
# If we have an HVAC mode ID from before the device was turned off, set it to
|
||||
# that mode
|
||||
if self._last_hvac_mode_id_before_off is not None:
|
||||
await self._async_set_value(
|
||||
self._current_mode, self._last_hvac_mode_id_before_off
|
||||
)
|
||||
self._last_hvac_mode_id_before_off = None
|
||||
return
|
||||
|
||||
# Attempt to set the device to the first available mode among heat_cool, heat,
|
||||
# and cool to mirror previous behavior. If none of those are available, set it
|
||||
# to the first available mode that is not off.
|
||||
try:
|
||||
hvac_mode = next(
|
||||
mode
|
||||
for mode in (HVACMode.HEAT_COOL, HVACMode.HEAT, HVACMode.COOL)
|
||||
if mode in self._hvac_modes
|
||||
)
|
||||
except StopIteration:
|
||||
hvac_mode = next(mode for mode in self._hvac_modes if mode != HVACMode.OFF)
|
||||
await self.async_set_hvac_mode(hvac_mode)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new target preset mode."""
|
||||
assert self._current_mode is not None
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
"""Test the Z-Wave JS climate platform."""
|
||||
import copy
|
||||
|
||||
import pytest
|
||||
from zwave_js_server.const import CommandClass
|
||||
from zwave_js_server.const.command_class.thermostat import (
|
||||
|
@ -37,6 +39,8 @@ from homeassistant.const import (
|
|||
ATTR_ENTITY_ID,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
ATTR_TEMPERATURE,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
|
@ -89,6 +93,18 @@ async def test_thermostat_v2(
|
|||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
# Check that turning the device on is a no-op because it is already on
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(client.async_send_command.call_args_list) == 0
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
# Test setting hvac mode
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
|
@ -277,6 +293,68 @@ async def test_thermostat_v2(
|
|||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
# Test turning device off then on to see if the previous state is retained
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY},
|
||||
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": 64,
|
||||
"property": "mode",
|
||||
}
|
||||
assert args["value"] == 0
|
||||
|
||||
# Update state to off
|
||||
event = Event(
|
||||
type="value updated",
|
||||
data={
|
||||
"source": "node",
|
||||
"event": "value updated",
|
||||
"nodeId": 13,
|
||||
"args": {
|
||||
"commandClassName": "Thermostat Mode",
|
||||
"commandClass": 64,
|
||||
"endpoint": 1,
|
||||
"property": "mode",
|
||||
"propertyName": "mode",
|
||||
"newValue": 0,
|
||||
"prevValue": 3,
|
||||
},
|
||||
},
|
||||
)
|
||||
node.receive_event(event)
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
# Test turning device on
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY},
|
||||
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": 64,
|
||||
"property": "mode",
|
||||
}
|
||||
assert args["value"] == 3
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
# Test setting invalid fan mode
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
|
@ -304,6 +382,145 @@ async def test_thermostat_v2(
|
|||
assert "Error while refreshing value" in caplog.text
|
||||
|
||||
|
||||
async def test_thermostat_v2_turn_on_after_off(
|
||||
hass: HomeAssistant, client, climate_radio_thermostat_ct100_plus, integration
|
||||
) -> None:
|
||||
"""Test thermostat v2 command class entity that is turned on after starting off."""
|
||||
node = climate_radio_thermostat_ct100_plus
|
||||
|
||||
# Turn device off so we can test turning it back on to see if the turn on service
|
||||
# attempts to find a value to set
|
||||
event = Event(
|
||||
type="value updated",
|
||||
data={
|
||||
"source": "node",
|
||||
"event": "value updated",
|
||||
"nodeId": 13,
|
||||
"args": {
|
||||
"commandClassName": "Thermostat Mode",
|
||||
"commandClass": 64,
|
||||
"endpoint": 1,
|
||||
"property": "mode",
|
||||
"propertyName": "mode",
|
||||
"newValue": 0,
|
||||
"prevValue": 1,
|
||||
},
|
||||
},
|
||||
)
|
||||
node.receive_event(event)
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
# Test turning device on
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY},
|
||||
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": 64,
|
||||
"property": "mode",
|
||||
}
|
||||
assert args["value"] == 3
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
|
||||
async def test_thermostat_turn_on_after_off_no_heat_cool_auto(
|
||||
hass: HomeAssistant, client, aeotec_radiator_thermostat_state, integration
|
||||
) -> None:
|
||||
"""Test thermostat that is turned on after starting off w/o heat, cool, or auto."""
|
||||
node_state = copy.deepcopy(aeotec_radiator_thermostat_state)
|
||||
# Only allow off and dry modes so we can test fallback logic when turning HVAC on
|
||||
# without a last mode stored.
|
||||
value = next(
|
||||
value
|
||||
for value in node_state["values"]
|
||||
if value["commandClass"] == 64 and value["property"] == "mode"
|
||||
)
|
||||
value["metadata"]["states"] = {"0": "Off", "6": "Fan", "8": "Dry"}
|
||||
value["value"] = 0
|
||||
node = Node(client, node_state)
|
||||
client.driver.controller.emit("node added", {"node": node})
|
||||
await hass.async_block_till_done()
|
||||
entity_id = "climate.thermostat_hvac"
|
||||
assert hass.states.get(entity_id).state == HVACMode.OFF
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
# Test turning device on sets it to first available mode (Energy heat)
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
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"] == 4
|
||||
assert args["valueId"] == {
|
||||
"endpoint": 0,
|
||||
"commandClass": 64,
|
||||
"property": "mode",
|
||||
}
|
||||
assert args["value"] == 6
|
||||
|
||||
|
||||
async def test_thermostat_turn_on_after_off_with_resume(
|
||||
hass: HomeAssistant, client, aeotec_radiator_thermostat_state, integration
|
||||
) -> None:
|
||||
"""Test thermostat that is turned on after starting off with resume support."""
|
||||
node_state = copy.deepcopy(aeotec_radiator_thermostat_state)
|
||||
# Add resume thermostat mode so we can test that it prefers the resume mode
|
||||
value = next(
|
||||
value
|
||||
for value in node_state["values"]
|
||||
if value["commandClass"] == 64 and value["property"] == "mode"
|
||||
)
|
||||
value["metadata"]["states"] = {
|
||||
"0": "Off",
|
||||
"5": "Resume (on)",
|
||||
"6": "Fan",
|
||||
"8": "Dry",
|
||||
}
|
||||
value["value"] = 0
|
||||
node = Node(client, node_state)
|
||||
client.driver.controller.emit("node added", {"node": node})
|
||||
await hass.async_block_till_done()
|
||||
entity_id = "climate.thermostat_hvac"
|
||||
assert hass.states.get(entity_id).state == HVACMode.OFF
|
||||
|
||||
client.async_send_command.reset_mock()
|
||||
|
||||
# Test turning device on sends resume command
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
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"] == 4
|
||||
assert args["valueId"] == {
|
||||
"endpoint": 0,
|
||||
"commandClass": 64,
|
||||
"property": "mode",
|
||||
}
|
||||
assert args["value"] == 5
|
||||
|
||||
|
||||
async def test_thermostat_different_endpoints(
|
||||
hass: HomeAssistant,
|
||||
client,
|
||||
|
|
Loading…
Add table
Reference in a new issue