diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 877841ef2ff..35be1a0229c 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -214,6 +214,7 @@ DISCOVERY_SCHEMAS = [ 0x3131, 0x3337, # 14287 / 55258 / ZW4002 0x3533, # 58446 / ZWA4013 + 0x3138, # 14314 / ZW4002 }, product_type={0x4944}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, @@ -221,14 +222,6 @@ DISCOVERY_SCHEMAS = [ FanValueMapping(speeds=[(1, 32), (33, 66), (67, 99)]), ), ), - # GE/Jasco - In-Wall Smart Fan Control - 14314 / ZW4002 - ZWaveDiscoverySchema( - platform=Platform.FAN, - manufacturer_id={0x0063}, - product_id={0x3138}, - product_type={0x4944}, - primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, - ), # Leviton ZW4SF fan controllers using switch multilevel CC ZWaveDiscoverySchema( platform=Platform.FAN, diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 1a765288cc1..0e27ef5a66f 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -326,6 +326,12 @@ def ge_12730_state_fixture() -> dict[str, Any]: return load_json_object_fixture("fan_ge_12730_state.json", DOMAIN) +@pytest.fixture(name="jasco_14314_state", scope="package") +def jasco_14314_state_fixture() -> dict[str, Any]: + """Load the Jasco 14314 node state fixture data.""" + return load_json_object_fixture("fan_jasco_14314_state.json", DOMAIN) + + @pytest.fixture(name="enbrighten_58446_zwa4013_state", scope="package") def enbrighten_58446_zwa4013_state_fixture() -> dict[str, Any]: """Load the Enbrighten/GE 58446/zwa401 node state fixture data.""" @@ -1109,6 +1115,14 @@ def ge_12730_fixture(client, ge_12730_state) -> Node: return node +@pytest.fixture(name="jasco_14314") +def jasco_14314_fixture(client, jasco_14314_state) -> Node: + """Mock a Jasco 14314 fan controller node.""" + node = Node(client, copy.deepcopy(jasco_14314_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="enbrighten_58446_zwa4013") def enbrighten_58446_zwa4013_fixture(client, enbrighten_58446_zwa4013_state) -> Node: """Mock a Enbrighten_58446/zwa4013 fan controller node.""" diff --git a/tests/components/zwave_js/fixtures/fan_jasco_14314_state.json b/tests/components/zwave_js/fixtures/fan_jasco_14314_state.json new file mode 100644 index 00000000000..a66e82062fa --- /dev/null +++ b/tests/components/zwave_js/fixtures/fan_jasco_14314_state.json @@ -0,0 +1,450 @@ +{ + "nodeId": 24, + "index": 0, + "status": 4, + "ready": true, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 8, + "label": "Fan Switch" + } + }, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 99, + "productId": 12600, + "productType": 18756, + "firmwareVersion": "5.24", + "zwavePlusVersion": 1, + "deviceConfig": { + "manufacturer": "Jasco", + "manufacturerId": 99, + "label": "14314 / ZW4002", + "description": "In-Wall Fan Speed Control, 500S", + "devices": [ + { + "productType": "0x4944", + "productId": "0x3138" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "preferred": false, + "associations": {}, + "paramInformation": { + "_map": {} + }, + "compat": { + "mapBasicSet": "event" + }, + "metadata": { + "inclusion": "Press and release the top or bottom of the smart switch", + "exclusion": "Press and release the top or bottom of the smart switch", + "reset": "Quickly press the top button three times, then immediately press the bottom button three times. The LED will flash on/off 5 times when completed successfully", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/1937/Binder1.pdf" + } + }, + "label": "14314 / ZW4002", + + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0063:0x4944:0x3138:5.24", + "highestSecurityClass": -1, + "isControllerNode": false, + "keepAwake": false, + "protocol": 0, + "values": [ + { + "endpoint": 0, + "commandClass": 32, + "commandClassName": "Basic", + "property": "event", + "propertyName": "event", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Event value", + "min": 0, + "max": 255, + "stateful": false, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + }, + "value": true + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "restorePrevious", + "propertyName": "restorePrevious", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Restore previous value", + "states": { + "true": "Restore" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "LED Indicator", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "LED Indicator", + "default": 0, + "min": 0, + "max": 2, + "states": { + "0": "On when load is off", + "1": "On when load is on", + "2": "Always off" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 12600 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 18756 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 99 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version", + "stateful": true, + "secret": false + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions", + "stateful": true, + "secret": false + }, + "value": ["5.24"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "4.54" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + }, + "stateful": true, + "secret": false + }, + "value": 3 + } + ], + "endpoints": [ + { + "nodeId": 24, + "index": 0, + "installerIcon": 1024, + "userIcon": 1024, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 8, + "label": "Fan Switch" + } + }, + "commandClasses": [ + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + } + ] + } + ] +} diff --git a/tests/components/zwave_js/test_fan.py b/tests/components/zwave_js/test_fan.py index 25ab6a87200..f57f412f2ad 100644 --- a/tests/components/zwave_js/test_fan.py +++ b/tests/components/zwave_js/test_fan.py @@ -447,6 +447,99 @@ async def test_ge_12730_fan(hass: HomeAssistant, client, ge_12730, integration) assert state.state == STATE_UNKNOWN +async def test_jasco_14314_fan( + hass: HomeAssistant, client, jasco_14314, integration +) -> None: + """Test a Jasco 14314 fan with 3 fixed speeds.""" + node = jasco_14314 + node_id = 24 + entity_id = "fan.in_wall_fan_speed_control_500s" + + 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: + # low = 1-32, med = 33-66, high = 67-99 + percentages_to_zwave_speeds = [ + [[0], [0]], + [range(1, 34), range(1, 33)], # percentages 1-33 → zwave 1-32 + [range(34, 68), range(33, 67)], # percentages 34-67 → zwave 33-66 + [range(68, 101), range(67, 100)], # percentages 68-100 → zwave 67-99 + ] + + 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(33.3333, rel=1e-3) + assert state.attributes[ATTR_PRESET_MODES] == [] + + # Test value is None + 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": None, + "prevValue": 0, + "propertyName": "currentValue", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(entity_id) + assert state.state == STATE_UNKNOWN + + async def test_inovelli_lzw36( hass: HomeAssistant, client, inovelli_lzw36, integration ) -> None: