494 lines
16 KiB
Python
494 lines
16 KiB
Python
"""Web socket API for OpenZWave."""
|
|
from openzwavemqtt.const import (
|
|
ATTR_CODE_SLOT,
|
|
ATTR_LABEL,
|
|
ATTR_POSITION,
|
|
ATTR_VALUE,
|
|
EVENT_NODE_ADDED,
|
|
EVENT_NODE_CHANGED,
|
|
)
|
|
from openzwavemqtt.exceptions import NotFoundError, NotSupportedError
|
|
from openzwavemqtt.util.lock import clear_usercode, get_code_slots, set_usercode
|
|
from openzwavemqtt.util.node import (
|
|
get_config_parameters,
|
|
get_node_from_manager,
|
|
set_config_parameter,
|
|
)
|
|
import voluptuous as vol
|
|
import voluptuous_serialize
|
|
|
|
from homeassistant.components import websocket_api
|
|
from homeassistant.core import callback
|
|
from homeassistant.helpers import config_validation as cv
|
|
|
|
from .const import ATTR_CONFIG_PARAMETER, ATTR_CONFIG_VALUE, DOMAIN, MANAGER
|
|
from .lock import ATTR_USERCODE
|
|
|
|
DRY_RUN = "dry_run"
|
|
TYPE = "type"
|
|
ID = "id"
|
|
OZW_INSTANCE = "ozw_instance"
|
|
NODE_ID = "node_id"
|
|
PARAMETER = ATTR_CONFIG_PARAMETER
|
|
VALUE = ATTR_CONFIG_VALUE
|
|
SCHEMA = "schema"
|
|
|
|
ATTR_NODE_QUERY_STAGE = "node_query_stage"
|
|
ATTR_IS_ZWAVE_PLUS = "is_zwave_plus"
|
|
ATTR_IS_AWAKE = "is_awake"
|
|
ATTR_IS_FAILED = "is_failed"
|
|
ATTR_NODE_BAUD_RATE = "node_baud_rate"
|
|
ATTR_IS_BEAMING = "is_beaming"
|
|
ATTR_IS_FLIRS = "is_flirs"
|
|
ATTR_IS_ROUTING = "is_routing"
|
|
ATTR_IS_SECURITYV1 = "is_securityv1"
|
|
ATTR_NODE_BASIC_STRING = "node_basic_string"
|
|
ATTR_NODE_GENERIC_STRING = "node_generic_string"
|
|
ATTR_NODE_SPECIFIC_STRING = "node_specific_string"
|
|
ATTR_NODE_MANUFACTURER_NAME = "node_manufacturer_name"
|
|
ATTR_NODE_PRODUCT_NAME = "node_product_name"
|
|
ATTR_NEIGHBORS = "neighbors"
|
|
|
|
|
|
@callback
|
|
def async_register_api(hass):
|
|
"""Register all of our api endpoints."""
|
|
websocket_api.async_register_command(hass, websocket_get_instances)
|
|
websocket_api.async_register_command(hass, websocket_get_nodes)
|
|
websocket_api.async_register_command(hass, websocket_network_status)
|
|
websocket_api.async_register_command(hass, websocket_network_statistics)
|
|
websocket_api.async_register_command(hass, websocket_node_metadata)
|
|
websocket_api.async_register_command(hass, websocket_node_status)
|
|
websocket_api.async_register_command(hass, websocket_node_statistics)
|
|
websocket_api.async_register_command(hass, websocket_refresh_node_info)
|
|
websocket_api.async_register_command(hass, websocket_get_config_parameters)
|
|
websocket_api.async_register_command(hass, websocket_set_config_parameter)
|
|
websocket_api.async_register_command(hass, websocket_set_usercode)
|
|
websocket_api.async_register_command(hass, websocket_clear_usercode)
|
|
websocket_api.async_register_command(hass, websocket_get_code_slots)
|
|
|
|
|
|
def _call_util_function(hass, connection, msg, send_result, function, *args):
|
|
"""Call an openzwavemqtt.util function."""
|
|
try:
|
|
node = get_node_from_manager(
|
|
hass.data[DOMAIN][MANAGER], msg[OZW_INSTANCE], msg[NODE_ID]
|
|
)
|
|
except NotFoundError as err:
|
|
connection.send_error(
|
|
msg[ID],
|
|
websocket_api.const.ERR_NOT_FOUND,
|
|
err.args[0],
|
|
)
|
|
return
|
|
|
|
try:
|
|
payload = function(node, *args)
|
|
except NotFoundError as err:
|
|
connection.send_error(
|
|
msg[ID],
|
|
websocket_api.const.ERR_NOT_FOUND,
|
|
err.args[0],
|
|
)
|
|
return
|
|
except NotSupportedError as err:
|
|
connection.send_error(
|
|
msg[ID],
|
|
websocket_api.const.ERR_NOT_SUPPORTED,
|
|
err.args[0],
|
|
)
|
|
return
|
|
|
|
if send_result:
|
|
connection.send_result(
|
|
msg[ID],
|
|
payload,
|
|
)
|
|
return
|
|
|
|
connection.send_result(msg[ID])
|
|
|
|
|
|
def _get_config_params(node, *args):
|
|
raw_values = get_config_parameters(node)
|
|
config_params = []
|
|
|
|
for param in raw_values:
|
|
schema = {}
|
|
|
|
if param["type"] in ("Byte", "Int", "Short"):
|
|
schema = vol.Schema(
|
|
{
|
|
vol.Required(param["label"], default=param["value"]): vol.All(
|
|
vol.Coerce(int), vol.Range(min=param["min"], max=param["max"])
|
|
)
|
|
}
|
|
)
|
|
data = {param["label"]: param["value"]}
|
|
|
|
if param["type"] == "List":
|
|
|
|
for options in param["options"]:
|
|
if options["Label"] == param["value"]:
|
|
selected = options
|
|
break
|
|
|
|
schema = vol.Schema(
|
|
{
|
|
vol.Required(param["label"],): vol.In(
|
|
{
|
|
option["Value"]: option["Label"]
|
|
for option in param["options"]
|
|
}
|
|
)
|
|
}
|
|
)
|
|
data = {param["label"]: selected["Value"]}
|
|
|
|
config_params.append(
|
|
{
|
|
"type": param["type"],
|
|
"label": param["label"],
|
|
"parameter": param["parameter"],
|
|
"help": param["help"],
|
|
"value": param["value"],
|
|
"schema": voluptuous_serialize.convert(
|
|
schema, custom_serializer=cv.custom_serializer
|
|
),
|
|
"data": data,
|
|
}
|
|
)
|
|
|
|
return config_params
|
|
|
|
|
|
@websocket_api.websocket_command({vol.Required(TYPE): "ozw/get_instances"})
|
|
def websocket_get_instances(hass, connection, msg):
|
|
"""Get a list of OZW instances."""
|
|
manager = hass.data[DOMAIN][MANAGER]
|
|
instances = []
|
|
|
|
for instance in manager.collections["instance"]:
|
|
instances.append(dict(instance.get_status().data, ozw_instance=instance.id))
|
|
|
|
connection.send_result(
|
|
msg[ID],
|
|
instances,
|
|
)
|
|
|
|
|
|
@websocket_api.websocket_command(
|
|
{
|
|
vol.Required(TYPE): "ozw/get_nodes",
|
|
vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
|
|
}
|
|
)
|
|
def websocket_get_nodes(hass, connection, msg):
|
|
"""Get a list of nodes for an OZW instance."""
|
|
manager = hass.data[DOMAIN][MANAGER]
|
|
nodes = []
|
|
|
|
for node in manager.get_instance(msg[OZW_INSTANCE]).collections["node"]:
|
|
nodes.append(
|
|
{
|
|
ATTR_NODE_QUERY_STAGE: node.node_query_stage,
|
|
NODE_ID: node.node_id,
|
|
ATTR_IS_ZWAVE_PLUS: node.is_zwave_plus,
|
|
ATTR_IS_AWAKE: node.is_awake,
|
|
ATTR_IS_FAILED: node.is_failed,
|
|
ATTR_NODE_BAUD_RATE: node.node_baud_rate,
|
|
ATTR_IS_BEAMING: node.is_beaming,
|
|
ATTR_IS_FLIRS: node.is_flirs,
|
|
ATTR_IS_ROUTING: node.is_routing,
|
|
ATTR_IS_SECURITYV1: node.is_securityv1,
|
|
ATTR_NODE_BASIC_STRING: node.node_basic_string,
|
|
ATTR_NODE_GENERIC_STRING: node.node_generic_string,
|
|
ATTR_NODE_SPECIFIC_STRING: node.node_specific_string,
|
|
ATTR_NODE_MANUFACTURER_NAME: node.node_manufacturer_name,
|
|
ATTR_NODE_PRODUCT_NAME: node.node_product_name,
|
|
ATTR_NEIGHBORS: node.neighbors,
|
|
OZW_INSTANCE: msg[OZW_INSTANCE],
|
|
}
|
|
)
|
|
|
|
connection.send_result(
|
|
msg[ID],
|
|
nodes,
|
|
)
|
|
|
|
|
|
@websocket_api.websocket_command(
|
|
{
|
|
vol.Required(TYPE): "ozw/set_usercode",
|
|
vol.Required(NODE_ID): vol.Coerce(int),
|
|
vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
|
|
vol.Required(ATTR_CODE_SLOT): vol.Coerce(int),
|
|
vol.Required(ATTR_USERCODE): cv.string,
|
|
}
|
|
)
|
|
def websocket_set_usercode(hass, connection, msg):
|
|
"""Set a usercode to a node code slot."""
|
|
_call_util_function(
|
|
hass, connection, msg, False, set_usercode, msg[ATTR_CODE_SLOT], ATTR_USERCODE
|
|
)
|
|
|
|
|
|
@websocket_api.websocket_command(
|
|
{
|
|
vol.Required(TYPE): "ozw/clear_usercode",
|
|
vol.Required(NODE_ID): vol.Coerce(int),
|
|
vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
|
|
vol.Required(ATTR_CODE_SLOT): vol.Coerce(int),
|
|
}
|
|
)
|
|
def websocket_clear_usercode(hass, connection, msg):
|
|
"""Clear a node code slot."""
|
|
_call_util_function(
|
|
hass, connection, msg, False, clear_usercode, msg[ATTR_CODE_SLOT]
|
|
)
|
|
|
|
|
|
@websocket_api.websocket_command(
|
|
{
|
|
vol.Required(TYPE): "ozw/get_code_slots",
|
|
vol.Required(NODE_ID): vol.Coerce(int),
|
|
vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
|
|
}
|
|
)
|
|
def websocket_get_code_slots(hass, connection, msg):
|
|
"""Get status of node's code slots."""
|
|
_call_util_function(hass, connection, msg, True, get_code_slots)
|
|
|
|
|
|
@websocket_api.websocket_command(
|
|
{
|
|
vol.Required(TYPE): "ozw/get_config_parameters",
|
|
vol.Required(NODE_ID): vol.Coerce(int),
|
|
vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
|
|
}
|
|
)
|
|
def websocket_get_config_parameters(hass, connection, msg):
|
|
"""Get a list of configuration parameters for an OZW node instance."""
|
|
_call_util_function(hass, connection, msg, True, _get_config_params)
|
|
|
|
|
|
@websocket_api.websocket_command(
|
|
{
|
|
vol.Required(TYPE): "ozw/set_config_parameter",
|
|
vol.Required(NODE_ID): vol.Coerce(int),
|
|
vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
|
|
vol.Required(PARAMETER): vol.Coerce(int),
|
|
vol.Required(VALUE): vol.Any(
|
|
vol.All(
|
|
cv.ensure_list,
|
|
[
|
|
vol.All(
|
|
{
|
|
vol.Exclusive(ATTR_LABEL, "bit"): cv.string,
|
|
vol.Exclusive(ATTR_POSITION, "bit"): vol.Coerce(int),
|
|
vol.Required(ATTR_VALUE): bool,
|
|
},
|
|
cv.has_at_least_one_key(ATTR_LABEL, ATTR_POSITION),
|
|
)
|
|
],
|
|
),
|
|
vol.Coerce(int),
|
|
bool,
|
|
cv.string,
|
|
),
|
|
}
|
|
)
|
|
def websocket_set_config_parameter(hass, connection, msg):
|
|
"""Set a config parameter to a node."""
|
|
_call_util_function(
|
|
hass, connection, msg, True, set_config_parameter, msg[PARAMETER], msg[VALUE]
|
|
)
|
|
|
|
|
|
@websocket_api.websocket_command(
|
|
{
|
|
vol.Required(TYPE): "ozw/network_status",
|
|
vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
|
|
}
|
|
)
|
|
def websocket_network_status(hass, connection, msg):
|
|
"""Get Z-Wave network status."""
|
|
|
|
manager = hass.data[DOMAIN][MANAGER]
|
|
status = manager.get_instance(msg[OZW_INSTANCE]).get_status().data
|
|
connection.send_result(
|
|
msg[ID],
|
|
dict(status, ozw_instance=msg[OZW_INSTANCE]),
|
|
)
|
|
|
|
|
|
@websocket_api.websocket_command(
|
|
{
|
|
vol.Required(TYPE): "ozw/network_statistics",
|
|
vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
|
|
}
|
|
)
|
|
def websocket_network_statistics(hass, connection, msg):
|
|
"""Get Z-Wave network statistics."""
|
|
|
|
manager = hass.data[DOMAIN][MANAGER]
|
|
statistics = manager.get_instance(msg[OZW_INSTANCE]).get_statistics().data
|
|
node_count = len(
|
|
manager.get_instance(msg[OZW_INSTANCE]).collections["node"].collection
|
|
)
|
|
connection.send_result(
|
|
msg[ID],
|
|
dict(statistics, ozw_instance=msg[OZW_INSTANCE], node_count=node_count),
|
|
)
|
|
|
|
|
|
@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(hass, connection, msg):
|
|
"""Get the status for a Z-Wave node."""
|
|
try:
|
|
node = get_node_from_manager(
|
|
hass.data[DOMAIN][MANAGER], msg[OZW_INSTANCE], msg[NODE_ID]
|
|
)
|
|
except NotFoundError as err:
|
|
connection.send_error(
|
|
msg[ID],
|
|
websocket_api.const.ERR_NOT_FOUND,
|
|
err.args[0],
|
|
)
|
|
return
|
|
|
|
connection.send_result(
|
|
msg[ID],
|
|
{
|
|
ATTR_NODE_QUERY_STAGE: node.node_query_stage,
|
|
NODE_ID: node.node_id,
|
|
ATTR_IS_ZWAVE_PLUS: node.is_zwave_plus,
|
|
ATTR_IS_AWAKE: node.is_awake,
|
|
ATTR_IS_FAILED: node.is_failed,
|
|
ATTR_NODE_BAUD_RATE: node.node_baud_rate,
|
|
ATTR_IS_BEAMING: node.is_beaming,
|
|
ATTR_IS_FLIRS: node.is_flirs,
|
|
ATTR_IS_ROUTING: node.is_routing,
|
|
ATTR_IS_SECURITYV1: node.is_securityv1,
|
|
ATTR_NODE_BASIC_STRING: node.node_basic_string,
|
|
ATTR_NODE_GENERIC_STRING: node.node_generic_string,
|
|
ATTR_NODE_SPECIFIC_STRING: node.node_specific_string,
|
|
ATTR_NODE_MANUFACTURER_NAME: node.node_manufacturer_name,
|
|
ATTR_NODE_PRODUCT_NAME: node.node_product_name,
|
|
ATTR_NEIGHBORS: node.neighbors,
|
|
OZW_INSTANCE: msg[OZW_INSTANCE],
|
|
},
|
|
)
|
|
|
|
|
|
@websocket_api.websocket_command(
|
|
{
|
|
vol.Required(TYPE): "ozw/node_metadata",
|
|
vol.Required(NODE_ID): vol.Coerce(int),
|
|
vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
|
|
}
|
|
)
|
|
def websocket_node_metadata(hass, connection, msg):
|
|
"""Get the metadata for a Z-Wave node."""
|
|
try:
|
|
node = get_node_from_manager(
|
|
hass.data[DOMAIN][MANAGER], msg[OZW_INSTANCE], msg[NODE_ID]
|
|
)
|
|
except NotFoundError as err:
|
|
connection.send_error(
|
|
msg[ID],
|
|
websocket_api.const.ERR_NOT_FOUND,
|
|
err.args[0],
|
|
)
|
|
return
|
|
|
|
connection.send_result(
|
|
msg[ID],
|
|
{
|
|
"metadata": node.meta_data,
|
|
NODE_ID: node.node_id,
|
|
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(hass, connection, msg):
|
|
"""Get the statistics for a Z-Wave node."""
|
|
manager = hass.data[DOMAIN][MANAGER]
|
|
stats = (
|
|
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],
|
|
},
|
|
)
|
|
|
|
|
|
@websocket_api.require_admin
|
|
@websocket_api.websocket_command(
|
|
{
|
|
vol.Required(TYPE): "ozw/refresh_node_info",
|
|
vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int),
|
|
vol.Required(NODE_ID): vol.Coerce(int),
|
|
}
|
|
)
|
|
def websocket_refresh_node_info(hass, connection, msg):
|
|
"""Tell OpenZWave to re-interview a node."""
|
|
|
|
manager = hass.data[DOMAIN][MANAGER]
|
|
options = manager.options
|
|
|
|
@callback
|
|
def forward_node(node):
|
|
"""Forward node events to websocket."""
|
|
if node.node_id != msg[NODE_ID]:
|
|
return
|
|
|
|
forward_data = {
|
|
"type": "node_updated",
|
|
ATTR_NODE_QUERY_STAGE: node.node_query_stage,
|
|
}
|
|
connection.send_message(websocket_api.event_message(msg["id"], forward_data))
|
|
|
|
@callback
|
|
def async_cleanup() -> None:
|
|
"""Remove signal listeners."""
|
|
for unsub in unsubs:
|
|
unsub()
|
|
|
|
connection.subscriptions[msg["id"]] = async_cleanup
|
|
unsubs = [
|
|
options.listen(EVENT_NODE_CHANGED, forward_node),
|
|
options.listen(EVENT_NODE_ADDED, forward_node),
|
|
]
|
|
|
|
instance = manager.get_instance(msg[OZW_INSTANCE])
|
|
instance.refresh_node(msg[NODE_ID])
|
|
connection.send_result(msg["id"])
|