Add zwave_js speed configuration for Leviton ZW4SF fans (#60677)
* Add speed info for Leviton 4 speed fans * Use new format for fan speed configuration * Add a fixture and test for the Leviton ZW4SF * Use pytest.approx
This commit is contained in:
parent
8084d163d3
commit
19cf5dfc6d
4 changed files with 9839 additions and 6 deletions
|
@ -289,10 +289,14 @@ DISCOVERY_SCHEMAS = [
|
||||||
# Leviton ZW4SF fan controllers using switch multilevel CC
|
# Leviton ZW4SF fan controllers using switch multilevel CC
|
||||||
ZWaveDiscoverySchema(
|
ZWaveDiscoverySchema(
|
||||||
platform=Platform.FAN,
|
platform=Platform.FAN,
|
||||||
|
hint="has_fan_value_mapping",
|
||||||
manufacturer_id={0x001D},
|
manufacturer_id={0x001D},
|
||||||
product_id={0x0002},
|
product_id={0x0002},
|
||||||
product_type={0x0038},
|
product_type={0x0038},
|
||||||
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
||||||
|
data_template=FixedFanValueMappingDataTemplate(
|
||||||
|
FanValueMapping(speeds=[(1, 25), (26, 50), (51, 75), (76, 99)]),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
# Inovelli LZW36 light / fan controller combo using switch multilevel CC
|
# Inovelli LZW36 light / fan controller combo using switch multilevel CC
|
||||||
# The fan is endpoint 2, the light is endpoint 1.
|
# The fan is endpoint 2, the light is endpoint 1.
|
||||||
|
|
|
@ -404,6 +404,12 @@ def hs_fc200_state_fixture():
|
||||||
return json.loads(load_fixture("zwave_js/fan_hs_fc200_state.json"))
|
return json.loads(load_fixture("zwave_js/fan_hs_fc200_state.json"))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="leviton_zw4sf_state", scope="session")
|
||||||
|
def leviton_zw4sf_state_fixture():
|
||||||
|
"""Load the Leviton ZW4SF node state fixture data."""
|
||||||
|
return json.loads(load_fixture("zwave_js/leviton_zw4sf_state.json"))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="gdc_zw062_state", scope="session")
|
@pytest.fixture(name="gdc_zw062_state", scope="session")
|
||||||
def motorized_barrier_cover_state_fixture():
|
def motorized_barrier_cover_state_fixture():
|
||||||
"""Load the motorized barrier cover node state fixture data."""
|
"""Load the motorized barrier cover node state fixture data."""
|
||||||
|
@ -874,6 +880,14 @@ def hs_fc200_fixture(client, hs_fc200_state):
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="leviton_zw4sf")
|
||||||
|
def leviton_zw4sf_fixture(client, leviton_zw4sf_state):
|
||||||
|
"""Mock a fan node."""
|
||||||
|
node = Node(client, copy.deepcopy(leviton_zw4sf_state))
|
||||||
|
client.driver.controller.nodes[node.node_id] = node
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="null_name_check")
|
@pytest.fixture(name="null_name_check")
|
||||||
def null_name_check_fixture(client, null_name_check_state):
|
def null_name_check_fixture(client, null_name_check_state):
|
||||||
"""Mock a node with no name."""
|
"""Mock a node with no name."""
|
||||||
|
|
9748
tests/components/zwave_js/fixtures/leviton_zw4sf_state.json
Normal file
9748
tests/components/zwave_js/fixtures/leviton_zw4sf_state.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,5 @@
|
||||||
"""Test the Z-Wave JS fan platform."""
|
"""Test the Z-Wave JS fan platform."""
|
||||||
import copy
|
import copy
|
||||||
import math
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from voluptuous.error import MultipleInvalid
|
from voluptuous.error import MultipleInvalid
|
||||||
|
@ -259,7 +258,7 @@ async def test_configurable_speeds_fan(hass, client, hs_fc200, integration):
|
||||||
assert actual_percentage in percentages
|
assert actual_percentage in percentages
|
||||||
|
|
||||||
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 state.attributes[ATTR_PERCENTAGE_STEP] == pytest.approx(33.3333, rel=1e-3)
|
||||||
assert state.attributes[ATTR_PRESET_MODES] == []
|
assert state.attributes[ATTR_PRESET_MODES] == []
|
||||||
|
|
||||||
|
|
||||||
|
@ -321,8 +320,8 @@ async def test_configurable_speeds_fan_with_bad_config_value(
|
||||||
assert state.state == STATE_UNAVAILABLE
|
assert state.state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
|
||||||
async def test_fixed_speeds_fan(hass, client, ge_12730, integration):
|
async def test_ge_12730_fan(hass, client, ge_12730, integration):
|
||||||
"""Test a fan entity with fixed speeds."""
|
"""Test a GE 12730 fan with 3 fixed speeds."""
|
||||||
node = ge_12730
|
node = ge_12730
|
||||||
node_id = 24
|
node_id = 24
|
||||||
entity_id = "fan.in_wall_smart_fan_control"
|
entity_id = "fan.in_wall_smart_fan_control"
|
||||||
|
@ -384,7 +383,7 @@ async def test_fixed_speeds_fan(hass, client, ge_12730, integration):
|
||||||
assert actual_percentage in percentages
|
assert actual_percentage in percentages
|
||||||
|
|
||||||
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 state.attributes[ATTR_PERCENTAGE_STEP] == pytest.approx(33.3333, rel=1e-3)
|
||||||
assert state.attributes[ATTR_PRESET_MODES] == []
|
assert state.attributes[ATTR_PRESET_MODES] == []
|
||||||
|
|
||||||
|
|
||||||
|
@ -456,7 +455,7 @@ async def test_inovelli_lzw36(hass, client, inovelli_lzw36, integration):
|
||||||
|
|
||||||
# Check static entity properties
|
# Check static entity properties
|
||||||
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 state.attributes[ATTR_PERCENTAGE_STEP] == pytest.approx(33.3333, rel=1e-3)
|
||||||
assert state.attributes[ATTR_PRESET_MODES] == ["breeze"]
|
assert state.attributes[ATTR_PRESET_MODES] == ["breeze"]
|
||||||
|
|
||||||
# This device has one preset, where a device level of "1" is the
|
# This device has one preset, where a device level of "1" is the
|
||||||
|
@ -491,6 +490,74 @@ async def test_inovelli_lzw36(hass, client, inovelli_lzw36, integration):
|
||||||
assert len(client.async_send_command.call_args_list) == 0
|
assert len(client.async_send_command.call_args_list) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_leviton_zw4sf_fan(hass, client, leviton_zw4sf, integration):
|
||||||
|
"""Test a Leviton ZW4SF fan with 4 fixed speeds."""
|
||||||
|
node = leviton_zw4sf
|
||||||
|
node_id = 88
|
||||||
|
entity_id = "fan.4_speed_fan_controller"
|
||||||
|
|
||||||
|
async def get_zwave_speed_from_percentage(percentage):
|
||||||
|
"""Set the fan to a particular percentage and get the resulting Zwave speed."""
|
||||||
|
client.async_send_command.reset_mock()
|
||||||
|
await hass.services.async_call(
|
||||||
|
"fan",
|
||||||
|
"turn_on",
|
||||||
|
{"entity_id": entity_id, "percentage": percentage},
|
||||||
|
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"] == node_id
|
||||||
|
return args["value"]
|
||||||
|
|
||||||
|
async def get_percentage_from_zwave_speed(zwave_speed):
|
||||||
|
"""Set the underlying device speed and get the resulting percentage."""
|
||||||
|
event = Event(
|
||||||
|
type="value updated",
|
||||||
|
data={
|
||||||
|
"source": "node",
|
||||||
|
"event": "value updated",
|
||||||
|
"nodeId": node_id,
|
||||||
|
"args": {
|
||||||
|
"commandClassName": "Multilevel Switch",
|
||||||
|
"commandClass": 38,
|
||||||
|
"endpoint": 0,
|
||||||
|
"property": "currentValue",
|
||||||
|
"newValue": zwave_speed,
|
||||||
|
"prevValue": 0,
|
||||||
|
"propertyName": "currentValue",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
node.receive_event(event)
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
return state.attributes[ATTR_PERCENTAGE]
|
||||||
|
|
||||||
|
# This device has the speeds:
|
||||||
|
# 1 = 1-25, 2 = 26-49, 3 = 50-74, 4 = 75-99
|
||||||
|
percentages_to_zwave_speeds = [
|
||||||
|
[[0], [0]],
|
||||||
|
[range(1, 26), range(1, 26)],
|
||||||
|
[range(26, 51), range(26, 51)],
|
||||||
|
[range(51, 76), range(51, 76)],
|
||||||
|
[range(76, 101), range(76, 100)],
|
||||||
|
]
|
||||||
|
|
||||||
|
for percentages, zwave_speeds in percentages_to_zwave_speeds:
|
||||||
|
for percentage in percentages:
|
||||||
|
actual_zwave_speed = await get_zwave_speed_from_percentage(percentage)
|
||||||
|
assert actual_zwave_speed in zwave_speeds
|
||||||
|
for zwave_speed in zwave_speeds:
|
||||||
|
actual_percentage = await get_percentage_from_zwave_speed(zwave_speed)
|
||||||
|
assert actual_percentage in percentages
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.attributes[ATTR_PERCENTAGE_STEP] == pytest.approx(25, rel=1e-3)
|
||||||
|
assert state.attributes[ATTR_PRESET_MODES] == []
|
||||||
|
|
||||||
|
|
||||||
async def test_thermostat_fan(hass, client, climate_adc_t3000, integration):
|
async def test_thermostat_fan(hass, client, climate_adc_t3000, integration):
|
||||||
"""Test the fan entity for a z-wave fan."""
|
"""Test the fan entity for a z-wave fan."""
|
||||||
node = climate_adc_t3000
|
node = climate_adc_t3000
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue