diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index c2a9ac7b3cf..e1a86115d50 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -3,7 +3,7 @@ DOMAIN = "zwave_js" NAME = "Z-Wave JS" -PLATFORMS = ["light", "sensor"] +PLATFORMS = ["light", "sensor", "switch"] DATA_CLIENT = "client" DATA_UNSUBSCRIBE = "unsubs" diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 34f715a2d40..39e9dd8d561 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -100,6 +100,12 @@ DISCOVERY_SCHEMAS = [ }, type={"number"}, ), + # binary switches + ZWaveDiscoverySchema( + platform="switch", + command_class={CommandClass.SWITCH_BINARY}, + property={"currentValue"}, + ), ] diff --git a/homeassistant/components/zwave_js/switch.py b/homeassistant/components/zwave_js/switch.py new file mode 100644 index 00000000000..e1606695599 --- /dev/null +++ b/homeassistant/components/zwave_js/switch.py @@ -0,0 +1,59 @@ +"""Representation of Z-Wave switches.""" + +import logging +from typing import Any, Callable, List + +from zwave_js_server.client import Client as ZwaveClient + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .discovery import ZwaveDiscoveryInfo +from .entity import ZWaveBaseEntity + +LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up Z-Wave sensor from config entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_switch(info: ZwaveDiscoveryInfo) -> None: + """Add Z-Wave Switch.""" + entities: List[ZWaveBaseEntity] = [] + entities.append(ZWaveSwitch(client, info)) + + async_add_entities(entities) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, f"{DOMAIN}_add_{SWITCH_DOMAIN}", async_add_switch + ) + ) + + +class ZWaveSwitch(ZWaveBaseEntity, SwitchEntity): + """Representation of a Z-Wave switch.""" + + @property + def is_on(self) -> bool: + """Return a boolean for the state of the switch.""" + return bool(self.info.primary_value.value) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + target_value = self.get_zwave_value("targetValue") + if target_value is not None: + await self.info.node.async_set_value(target_value, True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + target_value = self.get_zwave_value("targetValue") + if target_value is not None: + await self.info.node.async_set_value(target_value, False) diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index 8d2a2195913..c78c6d544b6 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -1,2 +1,3 @@ """Provide common test tools for Z-Wave JS.""" AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature" +SWITCH_ENTITY = "switch.smart_plug_with_two_usb_ports_current_value" diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 8dd24aaa3ab..389245b32a3 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -31,6 +31,12 @@ def multisensor_6_state_fixture(): return json.loads(load_fixture("zwave_js/multisensor_6_state.json")) +@pytest.fixture(name="hank_binary_switch_state", scope="session") +def binary_switch_state_fixture(): + """Load the hank binary switch node state fixture data.""" + return json.loads(load_fixture("zwave_js/hank_binary_switch_state.json")) + + @pytest.fixture(name="client") def mock_client_fixture(controller_state): """Mock a client.""" @@ -50,6 +56,14 @@ def multisensor_6_fixture(client, multisensor_6_state): return node +@pytest.fixture(name="hank_binary_switch") +def hank_binary_switch_fixture(client, hank_binary_switch_state): + """Mock a binary switch node.""" + node = Node(client, hank_binary_switch_state) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="integration") async def integration_fixture(hass, client): """Set up the zwave_js integration.""" diff --git a/tests/components/zwave_js/test_switch.py b/tests/components/zwave_js/test_switch.py new file mode 100644 index 00000000000..09d893a1906 --- /dev/null +++ b/tests/components/zwave_js/test_switch.py @@ -0,0 +1,85 @@ +"""Test the Z-Wave JS sensor platform.""" + +from zwave_js_server.event import Event + +from .common import SWITCH_ENTITY + + +async def test_switch(hass, hank_binary_switch, integration, client): + """Test the switch.""" + state = hass.states.get(SWITCH_ENTITY) + node = hank_binary_switch + + assert state + assert state.state == "off" + + # Test turning on + await hass.services.async_call( + "switch", "turn_on", {"entity_id": SWITCH_ENTITY}, blocking=True + ) + + args = client.async_send_json_message.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 32 + assert args["valueId"] == { + "commandClassName": "Binary Switch", + "commandClass": 37, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Target value", + }, + "value": False, + } + assert args["value"] is True + + # Test state updates from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 32, + "args": { + "commandClassName": "Binary Switch", + "commandClass": 37, + "endpoint": 0, + "property": "currentValue", + "newValue": True, + "prevValue": False, + "propertyName": "currentValue", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(SWITCH_ENTITY) + assert state.state == "on" + + # Test turning off + await hass.services.async_call( + "switch", "turn_off", {"entity_id": SWITCH_ENTITY}, blocking=True + ) + + args = client.async_send_json_message.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 32 + assert args["valueId"] == { + "commandClassName": "Binary Switch", + "commandClass": 37, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Target value", + }, + "value": False, + } + assert args["value"] is False diff --git a/tests/fixtures/zwave_js/hank_binary_switch_state.json b/tests/fixtures/zwave_js/hank_binary_switch_state.json new file mode 100644 index 00000000000..0c629b3cf99 --- /dev/null +++ b/tests/fixtures/zwave_js/hank_binary_switch_state.json @@ -0,0 +1,727 @@ +{ + "nodeId": 32, + "index": 0, + "installerIcon": 1792, + "userIcon": 1792, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Static Controller", + "generic": "Binary Switch", + "specific": "Binary Power Switch", + "mandatorySupportedCCs": [ + "Basic", + "Binary Switch", + "All Switch" + ], + "mandatoryControlCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 520, + "productId": 5, + "productType": 257, + "firmwareVersion": "1.5", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 5, + "deviceConfig": { + "manufacturerId": 520, + "manufacturer": "HANK Electronics Ltd.", + "label": "HKZW-SO01", + "description": "Smart Plug with two USB ports", + "devices": [ + { + "productType": "0x0101", + "productId": "0x0005" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + } + }, + "label": "HKZW-SO01", + "neighbors": [ + 1, + 33, + 36, + 37, + 39, + 52 + ], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 32, + "index": 0, + "installerIcon": 1792, + "userIcon": 1792 + } + ], + "values": [ + { + "commandClassName": "Binary Switch", + "commandClass": 37, + "endpoint": 0, + "property": "currentValue", + "propertyName": "currentValue", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value" + }, + "value": false + }, + { + "commandClassName": "Binary Switch", + "commandClass": 37, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value" + }, + "value": false + }, + { + "commandClassName": "Scene Activation", + "commandClass": 43, + "endpoint": 0, + "property": "sceneId", + "propertyName": "sceneId", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 1, + "max": 255, + "label": "Scene ID" + } + }, + { + "commandClassName": "Scene Activation", + "commandClass": 43, + "endpoint": 0, + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Dimming duration" + } + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "value", + "propertyKey": 66049, + "propertyName": "value", + "propertyKeyName": "W_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Value (Electric, Consumed)", + "unit": "W", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + } + }, + "value": 0 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "deltaTime", + "propertyKey": 66049, + "propertyName": "deltaTime", + "propertyKeyName": "W_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Time since the previous reading", + "unit": "s", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + } + }, + "value": 0 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "value", + "propertyKey": 65537, + "propertyName": "value", + "propertyKeyName": "kWh_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Value (Electric, Consumed)", + "unit": "kWh", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + } + }, + "value": 0.164 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "previousValue", + "propertyKey": 65537, + "propertyName": "previousValue", + "propertyKeyName": "kWh_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Previous value (Electric, Consumed)", + "unit": "kWh", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + } + }, + "value": 0.164 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "deltaTime", + "propertyKey": 65537, + "propertyName": "deltaTime", + "propertyKeyName": "kWh_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Time since the previous reading", + "unit": "s", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + } + }, + "value": 30 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "value", + "propertyKey": 66561, + "propertyName": "value", + "propertyKeyName": "V_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Value (Electric, Consumed)", + "unit": "V", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 4 + } + }, + "value": 122.963 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "deltaTime", + "propertyKey": 66561, + "propertyName": "deltaTime", + "propertyKeyName": "V_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Time since the previous reading", + "unit": "s", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 4 + } + }, + "value": 0 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "value", + "propertyKey": 66817, + "propertyName": "value", + "propertyKeyName": "A_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Value (Electric, Consumed)", + "unit": "A", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 5 + } + }, + "value": 0 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "deltaTime", + "propertyKey": 66817, + "propertyName": "deltaTime", + "propertyKeyName": "A_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Time since the previous reading", + "unit": "s", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 5 + } + }, + "value": 0 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "reset", + "propertyName": "reset", + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Reset accumulated values" + } + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "previousValue", + "propertyKey": 66049, + "propertyName": "previousValue", + "propertyKeyName": "W_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Previous value (Electric, Consumed)", + "unit": "W", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + } + } + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "previousValue", + "propertyKey": 66561, + "propertyName": "previousValue", + "propertyKeyName": "V_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Previous value (Electric, Consumed)", + "unit": "V", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 4 + } + } + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "previousValue", + "propertyKey": 66817, + "propertyName": "previousValue", + "propertyKeyName": "A_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Previous value (Electric, Consumed)", + "unit": "A", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 5 + } + } + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 20, + "propertyName": "Overload Protection", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Overload Protection", + "description": "If current exceeds 16.5A over 5 seconds, relay will turn off.", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 21, + "propertyName": "Device Status after Power Failure", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 2, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Device Status after Power Failure", + "description": "Define how the plug reacts after power failure", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 24, + "propertyName": "Notifcation on Load Change", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 2, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Notifcation on Load Change", + "description": "Smart Plug can send notifications to association device load state changes.", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 27, + "propertyName": "Indicator Modes", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Indicator Modes", + "description": "LED in the device will indicate the state of load", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 151, + "propertyName": "Threshold of power report", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 65535, + "default": 50, + "format": 1, + "allowManualEntry": true, + "label": "Threshold of power report", + "description": "Power Threshold at which to send meter report", + "isFromConfig": true + }, + "value": 50 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 152, + "propertyName": "Percentage Threshold of Power Report", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 10, + "format": 1, + "allowManualEntry": true, + "label": "Percentage Threshold of Power Report", + "description": "Percentage Threshold at which to send meter report", + "isFromConfig": true + }, + "value": 10 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 171, + "propertyName": "Power Report Frequency", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 5, + "max": 2678400, + "default": 30, + "format": 0, + "allowManualEntry": true, + "label": "Power Report Frequency", + "description": "The interval of sending power report to association device (Group Lifeline).", + "isFromConfig": true + }, + "value": 30 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 172, + "propertyName": "Energy Report Frequency", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 5, + "max": 2678400, + "default": 300, + "format": 0, + "allowManualEntry": true, + "label": "Energy Report Frequency", + "description": "The interval of sending energy report to association device (Group Lifeline).", + "isFromConfig": true + }, + "value": 300 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 173, + "propertyName": "Voltage Report Frequency", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 2678400, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Voltage Report Frequency", + "description": "The interval of sending voltage report to association device (Group Lifeline)", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 174, + "propertyName": "Electricity Report Frequency", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 2678400, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Electricity Report Frequency", + "description": "Interval for sending electricity report.", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 520 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 257 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 5 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Libary type" + }, + "value": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.24" + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "1.5" + ] + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ] +} \ No newline at end of file