Add Z-Wave thermostat fan entity (#65865)
* Add Z-Wave thermostat fan entity * Fix failing test, increase number of entities to 27 * Add tests to improve coverage * Take back unrelated changes to climate.py * Clean up guard clauses, use info.primary_value, and make entity disabled by default * Fix tests * Add more tests for code coverage * Remove unused const * Remove speed parameter from overridden method since it was removed from entity * Address PR comments
This commit is contained in:
parent
4b963c2ac0
commit
21aa07e3e5
5 changed files with 517 additions and 3 deletions
|
@ -34,6 +34,7 @@ from zwave_js_server.const.command_class.sound_switch import (
|
||||||
)
|
)
|
||||||
from zwave_js_server.const.command_class.thermostat import (
|
from zwave_js_server.const.command_class.thermostat import (
|
||||||
THERMOSTAT_CURRENT_TEMP_PROPERTY,
|
THERMOSTAT_CURRENT_TEMP_PROPERTY,
|
||||||
|
THERMOSTAT_FAN_MODE_PROPERTY,
|
||||||
THERMOSTAT_MODE_PROPERTY,
|
THERMOSTAT_MODE_PROPERTY,
|
||||||
THERMOSTAT_SETPOINT_PROPERTY,
|
THERMOSTAT_SETPOINT_PROPERTY,
|
||||||
)
|
)
|
||||||
|
@ -510,6 +511,17 @@ DISCOVERY_SCHEMAS = [
|
||||||
type={"any"},
|
type={"any"},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
# thermostat fan
|
||||||
|
ZWaveDiscoverySchema(
|
||||||
|
platform="fan",
|
||||||
|
hint="thermostat_fan",
|
||||||
|
primary_value=ZWaveValueDiscoverySchema(
|
||||||
|
command_class={CommandClass.THERMOSTAT_FAN_MODE},
|
||||||
|
property={THERMOSTAT_FAN_MODE_PROPERTY},
|
||||||
|
type={"number"},
|
||||||
|
),
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
# humidifier
|
# humidifier
|
||||||
# hygrostats supporting mode (and optional setpoint)
|
# hygrostats supporting mode (and optional setpoint)
|
||||||
ZWaveDiscoverySchema(
|
ZWaveDiscoverySchema(
|
||||||
|
|
|
@ -5,15 +5,22 @@ import math
|
||||||
from typing import Any, cast
|
from typing import Any, 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 TARGET_VALUE_PROPERTY
|
from zwave_js_server.const import TARGET_VALUE_PROPERTY, CommandClass
|
||||||
|
from zwave_js_server.const.command_class.thermostat import (
|
||||||
|
THERMOSTAT_FAN_OFF_PROPERTY,
|
||||||
|
THERMOSTAT_FAN_STATE_PROPERTY,
|
||||||
|
)
|
||||||
|
from zwave_js_server.model.value import Value as ZwaveValue
|
||||||
|
|
||||||
from homeassistant.components.fan import (
|
from homeassistant.components.fan import (
|
||||||
DOMAIN as FAN_DOMAIN,
|
DOMAIN as FAN_DOMAIN,
|
||||||
|
SUPPORT_PRESET_MODE,
|
||||||
SUPPORT_SET_SPEED,
|
SUPPORT_SET_SPEED,
|
||||||
FanEntity,
|
FanEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util.percentage import (
|
from homeassistant.util.percentage import (
|
||||||
|
@ -26,11 +33,14 @@ from .const import DATA_CLIENT, DOMAIN
|
||||||
from .discovery import ZwaveDiscoveryInfo
|
from .discovery import ZwaveDiscoveryInfo
|
||||||
from .discovery_data_template import FanSpeedDataTemplate
|
from .discovery_data_template import FanSpeedDataTemplate
|
||||||
from .entity import ZWaveBaseEntity
|
from .entity import ZWaveBaseEntity
|
||||||
|
from .helpers import get_value_of_zwave_value
|
||||||
|
|
||||||
SUPPORTED_FEATURES = SUPPORT_SET_SPEED
|
SUPPORTED_FEATURES = SUPPORT_SET_SPEED
|
||||||
|
|
||||||
DEFAULT_SPEED_RANGE = (1, 99) # off is not included
|
DEFAULT_SPEED_RANGE = (1, 99) # off is not included
|
||||||
|
|
||||||
|
ATTR_FAN_STATE = "fan_state"
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
@ -46,6 +56,8 @@ async def async_setup_entry(
|
||||||
entities: list[ZWaveBaseEntity] = []
|
entities: list[ZWaveBaseEntity] = []
|
||||||
if info.platform_hint == "configured_fan_speed":
|
if info.platform_hint == "configured_fan_speed":
|
||||||
entities.append(ConfiguredSpeedRangeZwaveFan(config_entry, client, info))
|
entities.append(ConfiguredSpeedRangeZwaveFan(config_entry, client, info))
|
||||||
|
elif info.platform_hint == "thermostat_fan":
|
||||||
|
entities.append(ZwaveThermostatFan(config_entry, client, info))
|
||||||
else:
|
else:
|
||||||
entities.append(ZwaveFan(config_entry, client, info))
|
entities.append(ZwaveFan(config_entry, client, info))
|
||||||
|
|
||||||
|
@ -224,3 +236,110 @@ class ConfiguredSpeedRangeZwaveFan(ZwaveFan):
|
||||||
# the UI handles steps e.g., for a 3-speed fan, you get steps at 33,
|
# the UI handles steps e.g., for a 3-speed fan, you get steps at 33,
|
||||||
# 67, and 100.
|
# 67, and 100.
|
||||||
return round(percentage)
|
return round(percentage)
|
||||||
|
|
||||||
|
|
||||||
|
class ZwaveThermostatFan(ZWaveBaseEntity, FanEntity):
|
||||||
|
"""Representation of a Z-Wave thermostat fan."""
|
||||||
|
|
||||||
|
_fan_mode: ZwaveValue
|
||||||
|
_fan_off: ZwaveValue | None = None
|
||||||
|
_fan_state: ZwaveValue | None = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the thermostat fan."""
|
||||||
|
super().__init__(config_entry, client, info)
|
||||||
|
|
||||||
|
self._fan_mode = self.info.primary_value
|
||||||
|
|
||||||
|
self._fan_off = self.get_zwave_value(
|
||||||
|
THERMOSTAT_FAN_OFF_PROPERTY,
|
||||||
|
CommandClass.THERMOSTAT_FAN_MODE,
|
||||||
|
add_to_watched_value_ids=True,
|
||||||
|
)
|
||||||
|
self._fan_state = self.get_zwave_value(
|
||||||
|
THERMOSTAT_FAN_STATE_PROPERTY,
|
||||||
|
CommandClass.THERMOSTAT_FAN_STATE,
|
||||||
|
add_to_watched_value_ids=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_turn_on(
|
||||||
|
self,
|
||||||
|
percentage: int | None = None,
|
||||||
|
preset_mode: str | None = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Turn the device on."""
|
||||||
|
if not self._fan_off:
|
||||||
|
raise HomeAssistantError("Unhandled action turn_on")
|
||||||
|
await self.info.node.async_set_value(self._fan_off, False)
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the device off."""
|
||||||
|
if not self._fan_off:
|
||||||
|
raise HomeAssistantError("Unhandled action turn_off")
|
||||||
|
await self.info.node.async_set_value(self._fan_off, True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool | None:
|
||||||
|
"""Return true if device is on."""
|
||||||
|
if (value := get_value_of_zwave_value(self._fan_off)) is None:
|
||||||
|
return None
|
||||||
|
return not cast(bool, value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def preset_mode(self) -> str | None:
|
||||||
|
"""Return the current preset mode, e.g., auto, smart, interval, favorite."""
|
||||||
|
value = get_value_of_zwave_value(self._fan_mode)
|
||||||
|
if value is None or str(value) not in self._fan_mode.metadata.states:
|
||||||
|
return None
|
||||||
|
return cast(str, self._fan_mode.metadata.states[str(value)])
|
||||||
|
|
||||||
|
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||||
|
"""Set new preset mode."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
new_state = next(
|
||||||
|
int(state)
|
||||||
|
for state, label in self._fan_mode.metadata.states.items()
|
||||||
|
if label == preset_mode
|
||||||
|
)
|
||||||
|
except StopIteration:
|
||||||
|
raise ValueError(f"Received an invalid fan mode: {preset_mode}") from None
|
||||||
|
|
||||||
|
await self.info.node.async_set_value(self._fan_mode, new_state)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def preset_modes(self) -> list[str] | None:
|
||||||
|
"""Return a list of available preset modes."""
|
||||||
|
if not self._fan_mode.metadata.states:
|
||||||
|
return None
|
||||||
|
return list(self._fan_mode.metadata.states.values())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self) -> int:
|
||||||
|
"""Flag supported features."""
|
||||||
|
return SUPPORT_PRESET_MODE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_state(self) -> str | None:
|
||||||
|
"""Return the current state, Idle, Running, etc."""
|
||||||
|
value = get_value_of_zwave_value(self._fan_state)
|
||||||
|
if (
|
||||||
|
value is None
|
||||||
|
or self._fan_state is None
|
||||||
|
or str(value) not in self._fan_state.metadata.states
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
return cast(str, self._fan_state.metadata.states[str(value)])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extra_state_attributes(self) -> dict[str, str] | None:
|
||||||
|
"""Return the optional state attributes."""
|
||||||
|
attrs = {}
|
||||||
|
|
||||||
|
if state := self.fan_state:
|
||||||
|
attrs[ATTR_FAN_STATE] = state
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
|
@ -676,6 +676,22 @@ def climate_adc_t3000_missing_mode_fixture(client, climate_adc_t3000_state):
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="climate_adc_t3000_missing_fan_mode_states")
|
||||||
|
def climate_adc_t3000_missing_fan_mode_states_fixture(client, climate_adc_t3000_state):
|
||||||
|
"""Mock a climate ADC-T3000 node with missing 'states' metadata on Thermostat Fan Mode."""
|
||||||
|
data = copy.deepcopy(climate_adc_t3000_state)
|
||||||
|
data["name"] = f"{data['name']} missing fan mode states"
|
||||||
|
for value in data["values"]:
|
||||||
|
if (
|
||||||
|
value["commandClassName"] == "Thermostat Fan Mode"
|
||||||
|
and value["property"] == "mode"
|
||||||
|
):
|
||||||
|
del value["metadata"]["states"]
|
||||||
|
node = Node(client, data)
|
||||||
|
client.driver.controller.nodes[node.node_id] = node
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="climate_danfoss_lc_13")
|
@pytest.fixture(name="climate_danfoss_lc_13")
|
||||||
def climate_danfoss_lc_13_fixture(client, climate_danfoss_lc_13_state):
|
def climate_danfoss_lc_13_fixture(client, climate_danfoss_lc_13_state):
|
||||||
"""Mock a climate radio danfoss LC-13 node."""
|
"""Mock a climate radio danfoss LC-13 node."""
|
||||||
|
|
|
@ -3,9 +3,30 @@ import math
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from voluptuous.error import MultipleInvalid
|
from voluptuous.error import MultipleInvalid
|
||||||
|
from zwave_js_server.const import CommandClass
|
||||||
from zwave_js_server.event import Event
|
from zwave_js_server.event import Event
|
||||||
|
|
||||||
from homeassistant.components.fan import ATTR_PERCENTAGE, ATTR_PERCENTAGE_STEP
|
from homeassistant.components.fan import (
|
||||||
|
ATTR_PERCENTAGE,
|
||||||
|
ATTR_PERCENTAGE_STEP,
|
||||||
|
ATTR_PRESET_MODE,
|
||||||
|
ATTR_PRESET_MODES,
|
||||||
|
DOMAIN as FAN_DOMAIN,
|
||||||
|
SERVICE_SET_PRESET_MODE,
|
||||||
|
SUPPORT_PRESET_MODE,
|
||||||
|
)
|
||||||
|
from homeassistant.components.zwave_js.fan import ATTR_FAN_STATE
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
ATTR_SUPPORTED_FEATURES,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_ON,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
)
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers import entity_registry
|
||||||
|
|
||||||
|
|
||||||
async def test_generic_fan(hass, client, fan_generic, integration):
|
async def test_generic_fan(hass, client, fan_generic, integration):
|
||||||
|
@ -304,3 +325,349 @@ async def test_fixed_speeds_fan(hass, client, ge_12730, integration):
|
||||||
|
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
assert math.isclose(state.attributes[ATTR_PERCENTAGE_STEP], 33.3333, rel_tol=1e-3)
|
assert math.isclose(state.attributes[ATTR_PERCENTAGE_STEP], 33.3333, rel_tol=1e-3)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_thermostat_fan(hass, client, climate_adc_t3000, integration):
|
||||||
|
"""Test the fan entity for a z-wave fan."""
|
||||||
|
node = climate_adc_t3000
|
||||||
|
entity_id = "fan.adc_t3000"
|
||||||
|
|
||||||
|
registry = entity_registry.async_get(hass)
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state is None
|
||||||
|
|
||||||
|
entry = registry.async_get(entity_id)
|
||||||
|
assert entry
|
||||||
|
assert entry.disabled
|
||||||
|
assert entry.disabled_by is entity_registry.RegistryEntryDisabler.INTEGRATION
|
||||||
|
|
||||||
|
# Test enabling entity
|
||||||
|
updated_entry = registry.async_update_entity(entity_id, disabled_by=None)
|
||||||
|
assert updated_entry != entry
|
||||||
|
assert updated_entry.disabled is False
|
||||||
|
|
||||||
|
await hass.config_entries.async_reload(integration.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
assert state.attributes.get(ATTR_FAN_STATE) == "Idle / off"
|
||||||
|
assert state.attributes.get(ATTR_PRESET_MODE) == "Auto low"
|
||||||
|
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == SUPPORT_PRESET_MODE
|
||||||
|
|
||||||
|
# Test setting preset mode
|
||||||
|
await hass.services.async_call(
|
||||||
|
FAN_DOMAIN,
|
||||||
|
SERVICE_SET_PRESET_MODE,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, ATTR_PRESET_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"] == 68
|
||||||
|
assert args["valueId"] == {
|
||||||
|
"ccVersion": 3,
|
||||||
|
"commandClassName": "Thermostat Fan Mode",
|
||||||
|
"commandClass": CommandClass.THERMOSTAT_FAN_MODE.value,
|
||||||
|
"endpoint": 0,
|
||||||
|
"property": "mode",
|
||||||
|
"propertyName": "mode",
|
||||||
|
"metadata": {
|
||||||
|
"label": "Thermostat fan mode",
|
||||||
|
"max": 255,
|
||||||
|
"min": 0,
|
||||||
|
"type": "number",
|
||||||
|
"readable": True,
|
||||||
|
"writeable": True,
|
||||||
|
"states": {"0": "Auto low", "1": "Low", "6": "Circulation"},
|
||||||
|
},
|
||||||
|
"value": 0,
|
||||||
|
}
|
||||||
|
assert args["value"] == 1
|
||||||
|
|
||||||
|
client.async_send_command.reset_mock()
|
||||||
|
|
||||||
|
# Test setting unknown preset mode
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await hass.services.async_call(
|
||||||
|
FAN_DOMAIN,
|
||||||
|
SERVICE_SET_PRESET_MODE,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "Turbo"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
client.async_send_command.reset_mock()
|
||||||
|
|
||||||
|
# Test turning off
|
||||||
|
await hass.services.async_call(
|
||||||
|
FAN_DOMAIN,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
{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"] == 68
|
||||||
|
assert args["valueId"] == {
|
||||||
|
"ccVersion": 3,
|
||||||
|
"commandClassName": "Thermostat Fan Mode",
|
||||||
|
"commandClass": CommandClass.THERMOSTAT_FAN_MODE.value,
|
||||||
|
"endpoint": 0,
|
||||||
|
"property": "off",
|
||||||
|
"propertyName": "off",
|
||||||
|
"metadata": {
|
||||||
|
"label": "Thermostat fan turned off",
|
||||||
|
"type": "boolean",
|
||||||
|
"readable": True,
|
||||||
|
"writeable": True,
|
||||||
|
},
|
||||||
|
"value": False,
|
||||||
|
}
|
||||||
|
assert args["value"]
|
||||||
|
|
||||||
|
client.async_send_command.reset_mock()
|
||||||
|
|
||||||
|
# Test turning on
|
||||||
|
await hass.services.async_call(
|
||||||
|
FAN_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"] == 68
|
||||||
|
assert args["valueId"] == {
|
||||||
|
"ccVersion": 3,
|
||||||
|
"commandClassName": "Thermostat Fan Mode",
|
||||||
|
"commandClass": CommandClass.THERMOSTAT_FAN_MODE.value,
|
||||||
|
"endpoint": 0,
|
||||||
|
"property": "off",
|
||||||
|
"propertyName": "off",
|
||||||
|
"metadata": {
|
||||||
|
"label": "Thermostat fan turned off",
|
||||||
|
"type": "boolean",
|
||||||
|
"readable": True,
|
||||||
|
"writeable": True,
|
||||||
|
},
|
||||||
|
"value": False,
|
||||||
|
}
|
||||||
|
assert not args["value"]
|
||||||
|
|
||||||
|
client.async_send_command.reset_mock()
|
||||||
|
|
||||||
|
# Test fan state update from value updated event
|
||||||
|
event = Event(
|
||||||
|
type="value updated",
|
||||||
|
data={
|
||||||
|
"source": "node",
|
||||||
|
"event": "value updated",
|
||||||
|
"nodeId": 68,
|
||||||
|
"args": {
|
||||||
|
"commandClassName": "Thermostat Fan State",
|
||||||
|
"commandClass": CommandClass.THERMOSTAT_FAN_STATE.value,
|
||||||
|
"endpoint": 0,
|
||||||
|
"property": "state",
|
||||||
|
"newValue": 4,
|
||||||
|
"prevValue": 0,
|
||||||
|
"propertyName": "state",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
node.receive_event(event)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.attributes.get(ATTR_FAN_STATE) == "Circulation mode"
|
||||||
|
|
||||||
|
client.async_send_command.reset_mock()
|
||||||
|
|
||||||
|
# Test unknown fan state update from value updated event
|
||||||
|
event = Event(
|
||||||
|
type="value updated",
|
||||||
|
data={
|
||||||
|
"source": "node",
|
||||||
|
"event": "value updated",
|
||||||
|
"nodeId": 68,
|
||||||
|
"args": {
|
||||||
|
"commandClassName": "Thermostat Fan State",
|
||||||
|
"commandClass": CommandClass.THERMOSTAT_FAN_STATE.value,
|
||||||
|
"endpoint": 0,
|
||||||
|
"property": "state",
|
||||||
|
"newValue": 99,
|
||||||
|
"prevValue": 0,
|
||||||
|
"propertyName": "state",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
node.receive_event(event)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert not state.attributes.get(ATTR_FAN_STATE)
|
||||||
|
|
||||||
|
client.async_send_command.reset_mock()
|
||||||
|
|
||||||
|
# Test fan mode update from value updated event
|
||||||
|
event = Event(
|
||||||
|
type="value updated",
|
||||||
|
data={
|
||||||
|
"source": "node",
|
||||||
|
"event": "value updated",
|
||||||
|
"nodeId": 68,
|
||||||
|
"args": {
|
||||||
|
"commandClassName": "Thermostat Fan Mode",
|
||||||
|
"commandClass": CommandClass.THERMOSTAT_FAN_MODE.value,
|
||||||
|
"endpoint": 0,
|
||||||
|
"property": "mode",
|
||||||
|
"newValue": 1,
|
||||||
|
"prevValue": 0,
|
||||||
|
"propertyName": "mode",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
node.receive_event(event)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.attributes.get(ATTR_PRESET_MODE) == "Low"
|
||||||
|
|
||||||
|
client.async_send_command.reset_mock()
|
||||||
|
|
||||||
|
# Test fan mode update from value updated event for an unknown mode
|
||||||
|
event = Event(
|
||||||
|
type="value updated",
|
||||||
|
data={
|
||||||
|
"source": "node",
|
||||||
|
"event": "value updated",
|
||||||
|
"nodeId": 68,
|
||||||
|
"args": {
|
||||||
|
"commandClassName": "Thermostat Fan Mode",
|
||||||
|
"commandClass": CommandClass.THERMOSTAT_FAN_MODE.value,
|
||||||
|
"endpoint": 0,
|
||||||
|
"property": "mode",
|
||||||
|
"newValue": 79,
|
||||||
|
"prevValue": 0,
|
||||||
|
"propertyName": "mode",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
node.receive_event(event)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert not state.attributes.get(ATTR_PRESET_MODE)
|
||||||
|
|
||||||
|
client.async_send_command.reset_mock()
|
||||||
|
|
||||||
|
# Test fan mode turned off update from value updated event
|
||||||
|
event = Event(
|
||||||
|
type="value updated",
|
||||||
|
data={
|
||||||
|
"source": "node",
|
||||||
|
"event": "value updated",
|
||||||
|
"nodeId": 68,
|
||||||
|
"args": {
|
||||||
|
"commandClassName": "Thermostat Fan Mode",
|
||||||
|
"commandClass": CommandClass.THERMOSTAT_FAN_MODE.value,
|
||||||
|
"endpoint": 0,
|
||||||
|
"property": "off",
|
||||||
|
"newValue": True,
|
||||||
|
"prevValue": False,
|
||||||
|
"propertyName": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
node.receive_event(event)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
|
async def test_thermostat_fan_without_off(
|
||||||
|
hass, client, climate_radio_thermostat_ct100_plus, integration
|
||||||
|
):
|
||||||
|
"""Test the fan entity for a z-wave fan without "off" property."""
|
||||||
|
entity_id = "fan.z_wave_thermostat"
|
||||||
|
|
||||||
|
registry = entity_registry.async_get(hass)
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state is None
|
||||||
|
|
||||||
|
entry = registry.async_get(entity_id)
|
||||||
|
assert entry
|
||||||
|
assert entry.disabled
|
||||||
|
assert entry.disabled_by is entity_registry.RegistryEntryDisabler.INTEGRATION
|
||||||
|
|
||||||
|
# Test enabling entity
|
||||||
|
updated_entry = registry.async_update_entity(entity_id, disabled_by=None)
|
||||||
|
assert updated_entry != entry
|
||||||
|
assert updated_entry.disabled is False
|
||||||
|
|
||||||
|
await hass.config_entries.async_reload(integration.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
# Test turning off
|
||||||
|
with pytest.raises(HomeAssistantError):
|
||||||
|
await hass.services.async_call(
|
||||||
|
FAN_DOMAIN,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
{ATTR_ENTITY_ID: entity_id},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(client.async_send_command.call_args_list) == 0
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
client.async_send_command.reset_mock()
|
||||||
|
|
||||||
|
# Test turning on
|
||||||
|
with pytest.raises(HomeAssistantError):
|
||||||
|
await hass.services.async_call(
|
||||||
|
FAN_DOMAIN,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{ATTR_ENTITY_ID: entity_id},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(client.async_send_command.call_args_list) == 0
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
client.async_send_command.reset_mock()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_thermostat_fan_without_preset_modes(
|
||||||
|
hass, client, climate_adc_t3000_missing_fan_mode_states, integration
|
||||||
|
):
|
||||||
|
"""Test the fan entity for a z-wave fan without "states" metadata."""
|
||||||
|
entity_id = "fan.adc_t3000_missing_fan_mode_states"
|
||||||
|
|
||||||
|
registry = entity_registry.async_get(hass)
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state is None
|
||||||
|
|
||||||
|
entry = registry.async_get(entity_id)
|
||||||
|
assert entry
|
||||||
|
assert entry.disabled
|
||||||
|
assert entry.disabled_by is entity_registry.RegistryEntryDisabler.INTEGRATION
|
||||||
|
|
||||||
|
# Test enabling entity
|
||||||
|
updated_entry = registry.async_update_entity(entity_id, disabled_by=None)
|
||||||
|
assert updated_entry != entry
|
||||||
|
assert updated_entry.disabled is False
|
||||||
|
|
||||||
|
await hass.config_entries.async_reload(integration.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
|
||||||
|
assert not state.attributes.get(ATTR_PRESET_MODE)
|
||||||
|
assert not state.attributes.get(ATTR_PRESET_MODES)
|
||||||
|
|
|
@ -811,7 +811,7 @@ async def test_removed_device(
|
||||||
# Check how many entities there are
|
# Check how many entities there are
|
||||||
ent_reg = er.async_get(hass)
|
ent_reg = er.async_get(hass)
|
||||||
entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id)
|
entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id)
|
||||||
assert len(entity_entries) == 28
|
assert len(entity_entries) == 29
|
||||||
|
|
||||||
# Remove a node and reload the entry
|
# Remove a node and reload the entry
|
||||||
old_node = client.driver.controller.nodes.pop(13)
|
old_node = client.driver.controller.nodes.pop(13)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue