From 13e8e287784d187f589ba81744ba334031c256ae Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Wed, 29 Jul 2020 15:35:26 -0500 Subject: [PATCH] Add basic websocket api for OZW (#38265) --- homeassistant/components/ozw/__init__.py | 5 + homeassistant/components/ozw/websocket_api.py | 114 ++++++++++++++++++ tests/components/ozw/test_websocket_api.py | 57 +++++++++ tests/fixtures/ozw/generic_network_dump.csv | 3 +- 4 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/ozw/websocket_api.py create mode 100644 tests/components/ozw/test_websocket_api.py diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index e7f6e0d3587..fa0eddfbcd1 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -35,6 +35,7 @@ from .entity import ( create_value_id, ) from .services import ZWaveServices +from .websocket_api import ZWaveWebsocketApi _LOGGER = logging.getLogger(__name__) @@ -206,6 +207,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): services = ZWaveServices(hass, manager) services.async_register() + # Register WebSocket API + ws_api = ZWaveWebsocketApi(hass, manager) + ws_api.async_register_api() + @callback def async_receive_message(msg): manager.receive_message(msg.topic, msg.payload) diff --git a/homeassistant/components/ozw/websocket_api.py b/homeassistant/components/ozw/websocket_api.py new file mode 100644 index 00000000000..3c11acb90d2 --- /dev/null +++ b/homeassistant/components/ozw/websocket_api.py @@ -0,0 +1,114 @@ +"""Web socket API for OpenZWave.""" + +import logging + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.core import callback + +_LOGGER = logging.getLogger(__name__) + +TYPE = "type" +ID = "id" +OZW_INSTANCE = "ozw_instance" +NODE_ID = "node_id" + + +class ZWaveWebsocketApi: + """Class that holds our websocket api commands.""" + + def __init__(self, hass, manager): + """Initialize with both hass and ozwmanager objects.""" + self._hass = hass + self._manager = manager + + @callback + def async_register_api(self): + """Register all of our api endpoints.""" + websocket_api.async_register_command(self._hass, self.websocket_network_status) + websocket_api.async_register_command(self._hass, self.websocket_node_status) + websocket_api.async_register_command(self._hass, self.websocket_node_statistics) + + @websocket_api.websocket_command( + { + vol.Required(TYPE): "ozw/network_status", + vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), + } + ) + def websocket_network_status(self, hass, connection, msg): + """Get Z-Wave network status.""" + + connection.send_result( + msg[ID], + { + "state": self._manager.get_instance(msg[OZW_INSTANCE]) + .get_status() + .status, + OZW_INSTANCE: msg[OZW_INSTANCE], + }, + ) + + @websocket_api.websocket_command( + { + vol.Required(TYPE): "ozw/node_status", + vol.Required(NODE_ID): vol.Coerce(int), + vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), + } + ) + def websocket_node_status(self, hass, connection, msg): + """Get the status for a Z-Wave node.""" + + node = self._manager.get_instance(msg[OZW_INSTANCE]).get_node(msg[NODE_ID]) + connection.send_result( + msg[ID], + { + "node_query_stage": node.node_query_stage, + "node_id": node.node_id, + "is_zwave_plus": node.is_zwave_plus, + "is_awake": node.is_awake, + "is_failed": node.is_failed, + "node_baud_rate": node.node_baud_rate, + "is_beaming": node.is_beaming, + "is_flirs": node.is_flirs, + "is_routing": node.is_routing, + "is_securityv1": node.is_securityv1, + "node_basic_string": node.node_basic_string, + "node_generic_string": node.node_generic_string, + "node_specific_string": node.node_specific_string, + OZW_INSTANCE: msg[OZW_INSTANCE], + }, + ) + + @websocket_api.websocket_command( + { + vol.Required(TYPE): "ozw/node_statistics", + vol.Required(NODE_ID): vol.Coerce(int), + vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), + } + ) + def websocket_node_statistics(self, hass, connection, msg): + """Get the statistics for a Z-Wave node.""" + + stats = ( + self._manager.get_instance(msg[OZW_INSTANCE]) + .get_node(msg[NODE_ID]) + .get_statistics() + ) + connection.send_result( + msg[ID], + { + "node_id": msg[NODE_ID], + "send_count": stats.send_count, + "sent_failed": stats.sent_failed, + "retries": stats.retries, + "last_request_rtt": stats.last_request_rtt, + "last_response_rtt": stats.last_response_rtt, + "average_request_rtt": stats.average_request_rtt, + "average_response_rtt": stats.average_response_rtt, + "received_packets": stats.received_packets, + "received_dup_packets": stats.received_dup_packets, + "received_unsolicited": stats.received_unsolicited, + OZW_INSTANCE: msg[OZW_INSTANCE], + }, + ) diff --git a/tests/components/ozw/test_websocket_api.py b/tests/components/ozw/test_websocket_api.py new file mode 100644 index 00000000000..7067e4ecd72 --- /dev/null +++ b/tests/components/ozw/test_websocket_api.py @@ -0,0 +1,57 @@ +"""Test OpenZWave Websocket API.""" + +from homeassistant.components.ozw.websocket_api import ID, NODE_ID, OZW_INSTANCE, TYPE + +from .common import setup_ozw + + +async def test_websocket_api(hass, generic_data, hass_ws_client): + """Test the ozw websocket api.""" + await setup_ozw(hass, fixture=generic_data) + client = await hass_ws_client(hass) + + # Test network status + await client.send_json({ID: 5, TYPE: "ozw/network_status"}) + msg = await client.receive_json() + result = msg["result"] + + assert result["state"] == "driverAllNodesQueried" + assert result[OZW_INSTANCE] == 1 + + # Test node status + await client.send_json({ID: 6, TYPE: "ozw/node_status", NODE_ID: 32}) + msg = await client.receive_json() + result = msg["result"] + + assert result[OZW_INSTANCE] == 1 + assert result[NODE_ID] == 32 + assert result["node_query_stage"] == "Complete" + assert result["is_zwave_plus"] + assert result["is_awake"] + assert not result["is_failed"] + assert result["node_baud_rate"] == 100000 + assert result["is_beaming"] + assert not result["is_flirs"] + assert result["is_routing"] + assert not result["is_securityv1"] + assert result["node_basic_string"] == "Routing Slave" + assert result["node_generic_string"] == "Binary Switch" + assert result["node_specific_string"] == "Binary Power Switch" + + # Test node statistics + await client.send_json({ID: 7, TYPE: "ozw/node_statistics", NODE_ID: 39}) + msg = await client.receive_json() + result = msg["result"] + + assert result[OZW_INSTANCE] == 1 + assert result[NODE_ID] == 39 + assert result["send_count"] == 57 + assert result["sent_failed"] == 0 + assert result["retries"] == 1 + assert result["last_request_rtt"] == 26 + assert result["last_response_rtt"] == 38 + assert result["average_request_rtt"] == 29 + assert result["average_response_rtt"] == 37 + assert result["received_packets"] == 3594 + assert result["received_dup_packets"] == 12 + assert result["received_unsolicited"] == 3546 diff --git a/tests/fixtures/ozw/generic_network_dump.csv b/tests/fixtures/ozw/generic_network_dump.csv index 9214796759a..a953121e881 100644 --- a/tests/fixtures/ozw/generic_network_dump.csv +++ b/tests/fixtures/ozw/generic_network_dump.csv @@ -279,4 +279,5 @@ OpenZWave/1/node/39/association/1/,{ "Name": "Lifeline", "Help": "", "M OpenZWave/1/node/39/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "TimeStamp": 1579566891} OpenZWave/1/node/39/instance/1/commandclass/43/value/562950622511127/,{ "Label": "Scene", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 7, "Genre": "User", "Help": "", "ValueIDKey": 122339347, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579630367} OpenZWave/1/node/39/instance/1/commandclass/91/,{ "Instance": 1, "CommandClassId": 91, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "TimeStamp": 1579630630} -OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/,{ "Label": "Scene 1", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 1, "Node": 61, "Genre": "User", "Help": "", "ValueIDKey": 281476005806100, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579640710} \ No newline at end of file +OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/,{ "Label": "Scene 1", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 1, "Node": 61, "Genre": "User", "Help": "", "ValueIDKey": 281476005806100, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579640710} +OpenZWave/1/node/39/statistics/,{ "sendCount": 57, "sentFailed": 0, "retries": 1, "receivedPackets": 3594, "receivedDupPackets": 12, "receivedUnsolicited": 3546, "lastSentTimeStamp": 1595764791, "lastReceivedTimeStamp": 1595802261, "lastRequestRTT": 26, "averageRequestRTT": 29, "lastResponseRTT": 38, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} \ No newline at end of file