core/homeassistant/components/zha/api.py

1104 lines
38 KiB
Python
Raw Normal View History

"""Web socket API for Zigbee Home Automation devices."""
import asyncio
import logging
import voluptuous as vol
from zigpy.types.named import EUI64
import zigpy.zdo.types as zdo_types
from homeassistant.components import websocket_api
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import async_get_registry
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .core.const import (
2019-07-31 19:25:30 +00:00
ATTR_ARGS,
ATTR_ATTRIBUTE,
ATTR_CLUSTER_ID,
ATTR_CLUSTER_TYPE,
ATTR_COMMAND,
ATTR_COMMAND_TYPE,
ATTR_ENDPOINT_ID,
ATTR_LEVEL,
2019-07-31 19:25:30 +00:00
ATTR_MANUFACTURER,
ATTR_MEMBERS,
2019-08-02 14:37:21 +00:00
ATTR_NAME,
2019-07-31 19:25:30 +00:00
ATTR_VALUE,
ATTR_WARNING_DEVICE_DURATION,
ATTR_WARNING_DEVICE_MODE,
ATTR_WARNING_DEVICE_STROBE,
ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE,
ATTR_WARNING_DEVICE_STROBE_INTENSITY,
CHANNEL_IAS_WD,
2019-08-02 14:37:21 +00:00
CLUSTER_COMMAND_SERVER,
CLUSTER_COMMANDS_CLIENT,
2019-08-02 14:37:21 +00:00
CLUSTER_COMMANDS_SERVER,
CLUSTER_TYPE_IN,
CLUSTER_TYPE_OUT,
2019-07-31 19:25:30 +00:00
DATA_ZHA,
DATA_ZHA_GATEWAY,
DOMAIN,
GROUP_ID,
GROUP_IDS,
GROUP_NAME,
2019-07-31 19:25:30 +00:00
MFG_CLUSTER_ID_START,
WARNING_DEVICE_MODE_EMERGENCY,
WARNING_DEVICE_SOUND_HIGH,
WARNING_DEVICE_SQUAWK_MODE_ARMED,
WARNING_DEVICE_STROBE_HIGH,
WARNING_DEVICE_STROBE_YES,
2019-07-31 19:25:30 +00:00
)
from .core.helpers import (
async_get_device_info,
async_is_bindable_target,
get_matched_clusters,
)
_LOGGER = logging.getLogger(__name__)
2019-07-31 19:25:30 +00:00
TYPE = "type"
CLIENT = "client"
ID = "id"
RESPONSE = "response"
DEVICE_INFO = "device_info"
ATTR_DURATION = "duration"
ATTR_GROUP = "group"
2019-07-31 19:25:30 +00:00
ATTR_IEEE_ADDRESS = "ieee_address"
ATTR_IEEE = "ieee"
ATTR_SOURCE_IEEE = "source_ieee"
ATTR_TARGET_IEEE = "target_ieee"
2019-07-31 19:25:30 +00:00
SERVICE_PERMIT = "permit"
SERVICE_REMOVE = "remove"
SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE = "set_zigbee_cluster_attribute"
SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND = "issue_zigbee_cluster_command"
SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND = "issue_zigbee_group_command"
2019-07-31 19:25:30 +00:00
SERVICE_DIRECT_ZIGBEE_BIND = "issue_direct_zigbee_bind"
SERVICE_DIRECT_ZIGBEE_UNBIND = "issue_direct_zigbee_unbind"
SERVICE_WARNING_DEVICE_SQUAWK = "warning_device_squawk"
SERVICE_WARNING_DEVICE_WARN = "warning_device_warn"
2019-07-31 19:25:30 +00:00
SERVICE_ZIGBEE_BIND = "service_zigbee_bind"
IEEE_SERVICE = "ieee_based_service"
SERVICE_SCHEMAS = {
2019-07-31 19:25:30 +00:00
SERVICE_PERMIT: vol.Schema(
{
vol.Optional(ATTR_IEEE_ADDRESS, default=None): EUI64.convert,
2019-07-31 19:25:30 +00:00
vol.Optional(ATTR_DURATION, default=60): vol.All(
vol.Coerce(int), vol.Range(0, 254)
),
}
),
IEEE_SERVICE: vol.Schema({vol.Required(ATTR_IEEE_ADDRESS): EUI64.convert}),
2019-07-31 19:25:30 +00:00
SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE: vol.Schema(
{
vol.Required(ATTR_IEEE): EUI64.convert,
2019-07-31 19:25:30 +00:00
vol.Required(ATTR_ENDPOINT_ID): cv.positive_int,
vol.Required(ATTR_CLUSTER_ID): cv.positive_int,
vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string,
2019-07-31 19:25:30 +00:00
vol.Required(ATTR_ATTRIBUTE): cv.positive_int,
vol.Required(ATTR_VALUE): cv.string,
vol.Optional(ATTR_MANUFACTURER): cv.positive_int,
}
),
SERVICE_WARNING_DEVICE_SQUAWK: vol.Schema(
{
vol.Required(ATTR_IEEE): EUI64.convert,
vol.Optional(
ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_SQUAWK_MODE_ARMED
): cv.positive_int,
vol.Optional(
ATTR_WARNING_DEVICE_STROBE, default=WARNING_DEVICE_STROBE_YES
): cv.positive_int,
vol.Optional(
ATTR_LEVEL, default=WARNING_DEVICE_SOUND_HIGH
): cv.positive_int,
}
),
SERVICE_WARNING_DEVICE_WARN: vol.Schema(
{
vol.Required(ATTR_IEEE): EUI64.convert,
vol.Optional(
ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_MODE_EMERGENCY
): cv.positive_int,
vol.Optional(
ATTR_WARNING_DEVICE_STROBE, default=WARNING_DEVICE_STROBE_YES
): cv.positive_int,
vol.Optional(
ATTR_LEVEL, default=WARNING_DEVICE_SOUND_HIGH
): cv.positive_int,
vol.Optional(ATTR_WARNING_DEVICE_DURATION, default=5): cv.positive_int,
vol.Optional(
ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE, default=0x00
): cv.positive_int,
vol.Optional(
ATTR_WARNING_DEVICE_STROBE_INTENSITY, default=WARNING_DEVICE_STROBE_HIGH
): cv.positive_int,
}
),
2019-07-31 19:25:30 +00:00
SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND: vol.Schema(
{
vol.Required(ATTR_IEEE): EUI64.convert,
2019-07-31 19:25:30 +00:00
vol.Required(ATTR_ENDPOINT_ID): cv.positive_int,
vol.Required(ATTR_CLUSTER_ID): cv.positive_int,
vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string,
2019-07-31 19:25:30 +00:00
vol.Required(ATTR_COMMAND): cv.positive_int,
vol.Required(ATTR_COMMAND_TYPE): cv.string,
vol.Optional(ATTR_ARGS, default=[]): cv.ensure_list,
2019-07-31 19:25:30 +00:00
vol.Optional(ATTR_MANUFACTURER): cv.positive_int,
}
),
SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND: vol.Schema(
{
vol.Required(ATTR_GROUP): cv.positive_int,
vol.Required(ATTR_CLUSTER_ID): cv.positive_int,
vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string,
vol.Required(ATTR_COMMAND): cv.positive_int,
vol.Optional(ATTR_ARGS, default=[]): cv.ensure_list,
vol.Optional(ATTR_MANUFACTURER): cv.positive_int,
}
),
}
@websocket_api.require_admin
@websocket_api.async_response
2019-07-31 19:25:30 +00:00
@websocket_api.websocket_command(
{
vol.Required("type"): "zha/devices/permit",
vol.Optional(ATTR_IEEE, default=None): EUI64.convert,
2019-07-31 19:25:30 +00:00
vol.Optional(ATTR_DURATION, default=60): vol.All(
vol.Coerce(int), vol.Range(0, 254)
),
}
)
async def websocket_permit_devices(hass, connection, msg):
"""Permit ZHA zigbee devices."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
duration = msg.get(ATTR_DURATION)
ieee = msg.get(ATTR_IEEE)
async def forward_messages(data):
"""Forward events to websocket."""
2019-07-31 19:25:30 +00:00
connection.send_message(websocket_api.event_message(msg["id"], data))
remove_dispatcher_function = async_dispatcher_connect(
2019-07-31 19:25:30 +00:00
hass, "zha_gateway_message", forward_messages
)
@callback
def async_cleanup() -> None:
"""Remove signal listener and turn off debug mode."""
zha_gateway.async_disable_debug_mode()
remove_dispatcher_function()
2019-07-31 19:25:30 +00:00
connection.subscriptions[msg["id"]] = async_cleanup
zha_gateway.async_enable_debug_mode()
2019-07-31 19:25:30 +00:00
await zha_gateway.application_controller.permit(time_s=duration, node=ieee)
connection.send_result(msg["id"])
@websocket_api.require_admin
2019-03-01 00:32:41 +00:00
@websocket_api.async_response
2019-07-31 19:25:30 +00:00
@websocket_api.websocket_command({vol.Required(TYPE): "zha/devices"})
2019-03-01 00:32:41 +00:00
async def websocket_get_devices(hass, connection, msg):
"""Get ZHA devices."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
ha_device_registry = await async_get_registry(hass)
2019-03-02 01:15:36 +00:00
devices = []
for device in zha_gateway.devices.values():
devices.append(
2019-07-31 19:25:30 +00:00
async_get_device_info(hass, device, ha_device_registry=ha_device_registry)
)
connection.send_result(msg[ID], devices)
2019-03-02 01:15:36 +00:00
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command({vol.Required(TYPE): "zha/devices/groupable"})
async def websocket_get_groupable_devices(hass, connection, msg):
"""Get ZHA devices that can be grouped."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
ha_device_registry = await async_get_registry(hass)
devices = []
for device in zha_gateway.devices.values():
if device.is_groupable:
devices.append(
async_get_device_info(
hass, device, ha_device_registry=ha_device_registry
)
)
connection.send_result(msg[ID], devices)
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command({vol.Required(TYPE): "zha/groups"})
async def websocket_get_groups(hass, connection, msg):
"""Get ZHA groups."""
groups = await get_groups(hass)
connection.send_result(msg[ID], groups)
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command(
{vol.Required(TYPE): "zha/device", vol.Required(ATTR_IEEE): EUI64.convert}
)
async def websocket_get_device(hass, connection, msg):
"""Get ZHA devices."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
ha_device_registry = await async_get_registry(hass)
ieee = msg[ATTR_IEEE]
device = None
if ieee in zha_gateway.devices:
device = async_get_device_info(
hass, zha_gateway.devices[ieee], ha_device_registry=ha_device_registry
)
if not device:
connection.send_message(
websocket_api.error_message(
msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Device not found"
)
)
return
connection.send_result(msg[ID], device)
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command(
{vol.Required(TYPE): "zha/group", vol.Required(GROUP_ID): cv.positive_int}
)
async def websocket_get_group(hass, connection, msg):
"""Get ZHA group."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
ha_device_registry = await async_get_registry(hass)
group_id = msg[GROUP_ID]
group = None
if group_id in zha_gateway.application_controller.groups:
group = async_get_group_info(
hass,
zha_gateway,
zha_gateway.application_controller.groups[group_id],
ha_device_registry,
)
if not group:
connection.send_message(
websocket_api.error_message(
msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found"
)
)
return
connection.send_result(msg[ID], group)
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zha/group/add",
vol.Required(GROUP_NAME): cv.string,
vol.Optional(ATTR_MEMBERS): vol.All(cv.ensure_list, [EUI64.convert]),
}
)
async def websocket_add_group(hass, connection, msg):
"""Add a new ZHA group."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
ha_device_registry = await async_get_registry(hass)
group_id = len(zha_gateway.application_controller.groups) + 1
group_name = msg[GROUP_NAME]
zigpy_group = async_get_group_by_name(zha_gateway, group_name)
ret_group = None
members = msg.get(ATTR_MEMBERS)
# guard against group already existing
if zigpy_group is None:
zigpy_group = zha_gateway.application_controller.groups.add_group(
group_id, group_name
)
if members is not None:
tasks = []
for ieee in members:
tasks.append(zha_gateway.devices[ieee].async_add_to_group(group_id))
await asyncio.gather(*tasks)
ret_group = async_get_group_info(hass, zha_gateway, zigpy_group, ha_device_registry)
connection.send_result(msg[ID], ret_group)
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zha/group/remove",
vol.Required(GROUP_IDS): vol.All(cv.ensure_list, [cv.positive_int]),
}
)
async def websocket_remove_groups(hass, connection, msg):
"""Remove the specified ZHA groups."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
groups = zha_gateway.application_controller.groups
group_ids = msg[GROUP_IDS]
if len(group_ids) > 1:
tasks = []
for group_id in group_ids:
tasks.append(remove_group(groups[group_id], zha_gateway))
await asyncio.gather(*tasks)
else:
await remove_group(groups[group_ids[0]], zha_gateway)
ret_groups = await get_groups(hass)
connection.send_result(msg[ID], ret_groups)
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zha/group/members/add",
vol.Required(GROUP_ID): cv.positive_int,
vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [EUI64.convert]),
}
)
async def websocket_add_group_members(hass, connection, msg):
"""Add members to a ZHA group."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
ha_device_registry = await async_get_registry(hass)
group_id = msg[GROUP_ID]
members = msg[ATTR_MEMBERS]
zigpy_group = None
if group_id in zha_gateway.application_controller.groups:
zigpy_group = zha_gateway.application_controller.groups[group_id]
tasks = []
for ieee in members:
tasks.append(zha_gateway.devices[ieee].async_add_to_group(group_id))
await asyncio.gather(*tasks)
if not zigpy_group:
connection.send_message(
websocket_api.error_message(
msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found"
)
)
return
ret_group = async_get_group_info(hass, zha_gateway, zigpy_group, ha_device_registry)
connection.send_result(msg[ID], ret_group)
@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zha/group/members/remove",
vol.Required(GROUP_ID): cv.positive_int,
vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [EUI64.convert]),
}
)
async def websocket_remove_group_members(hass, connection, msg):
"""Remove members from a ZHA group."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
ha_device_registry = await async_get_registry(hass)
group_id = msg[GROUP_ID]
members = msg[ATTR_MEMBERS]
zigpy_group = None
if group_id in zha_gateway.application_controller.groups:
zigpy_group = zha_gateway.application_controller.groups[group_id]
tasks = []
for ieee in members:
tasks.append(zha_gateway.devices[ieee].async_remove_from_group(group_id))
await asyncio.gather(*tasks)
if not zigpy_group:
connection.send_message(
websocket_api.error_message(
msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found"
)
)
return
ret_group = async_get_group_info(hass, zha_gateway, zigpy_group, ha_device_registry)
connection.send_result(msg[ID], ret_group)
async def get_groups(hass,):
"""Get ZHA Groups."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
ha_device_registry = await async_get_registry(hass)
groups = []
for group in zha_gateway.application_controller.groups.values():
groups.append(
async_get_group_info(hass, zha_gateway, group, ha_device_registry)
)
return groups
async def remove_group(group, zha_gateway):
"""Remove ZHA Group."""
if group.members:
tasks = []
for member_ieee in group.members.keys():
if member_ieee[0] in zha_gateway.devices:
tasks.append(
zha_gateway.devices[member_ieee[0]].async_remove_from_group(
group.group_id
)
)
if tasks:
await asyncio.gather(*tasks)
else:
# we have members but none are tracked by ZHA for whatever reason
zha_gateway.application_controller.groups.pop(group.group_id)
else:
zha_gateway.application_controller.groups.pop(group.group_id)
@callback
def async_get_group_info(hass, zha_gateway, group, ha_device_registry):
"""Get ZHA group."""
ret_group = {}
ret_group["group_id"] = group.group_id
ret_group["name"] = group.name
ret_group["members"] = [
async_get_device_info(
hass,
zha_gateway.get_device(member_ieee[0]),
ha_device_registry=ha_device_registry,
)
for member_ieee in group.members.keys()
if member_ieee[0] in zha_gateway.devices
]
return ret_group
@callback
def async_get_group_by_name(zha_gateway, group_name):
"""Get ZHA group by name."""
for group in zha_gateway.application_controller.groups.values():
if group.name == group_name:
return group
return None
@websocket_api.require_admin
2019-03-01 00:32:41 +00:00
@websocket_api.async_response
2019-07-31 19:25:30 +00:00
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zha/devices/reconfigure",
vol.Required(ATTR_IEEE): EUI64.convert,
2019-07-31 19:25:30 +00:00
}
)
2019-03-01 00:32:41 +00:00
async def websocket_reconfigure_node(hass, connection, msg):
"""Reconfigure a ZHA nodes entities by its ieee address."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
ieee = msg[ATTR_IEEE]
device = zha_gateway.get_device(ieee)
_LOGGER.debug("Reconfiguring node with ieee_address: %s", ieee)
hass.async_create_task(device.async_configure())
@websocket_api.require_admin
2019-03-01 00:32:41 +00:00
@websocket_api.async_response
2019-07-31 19:25:30 +00:00
@websocket_api.websocket_command(
{vol.Required(TYPE): "zha/devices/clusters", vol.Required(ATTR_IEEE): EUI64.convert}
2019-07-31 19:25:30 +00:00
)
2019-03-01 00:32:41 +00:00
async def websocket_device_clusters(hass, connection, msg):
"""Return a list of device clusters."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
ieee = msg[ATTR_IEEE]
zha_device = zha_gateway.get_device(ieee)
response_clusters = []
if zha_device is not None:
clusters_by_endpoint = zha_device.async_get_clusters()
for ep_id, clusters in clusters_by_endpoint.items():
for c_id, cluster in clusters[CLUSTER_TYPE_IN].items():
2019-07-31 19:25:30 +00:00
response_clusters.append(
{
TYPE: CLUSTER_TYPE_IN,
2019-07-31 19:25:30 +00:00
ID: c_id,
ATTR_NAME: cluster.__class__.__name__,
2019-07-31 19:25:30 +00:00
"endpoint_id": ep_id,
}
)
for c_id, cluster in clusters[CLUSTER_TYPE_OUT].items():
2019-07-31 19:25:30 +00:00
response_clusters.append(
{
TYPE: CLUSTER_TYPE_OUT,
2019-07-31 19:25:30 +00:00
ID: c_id,
ATTR_NAME: cluster.__class__.__name__,
2019-07-31 19:25:30 +00:00
"endpoint_id": ep_id,
}
)
2019-03-01 00:32:41 +00:00
connection.send_result(msg[ID], response_clusters)
@websocket_api.require_admin
2019-03-01 00:32:41 +00:00
@websocket_api.async_response
2019-07-31 19:25:30 +00:00
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zha/devices/clusters/attributes",
vol.Required(ATTR_IEEE): EUI64.convert,
2019-07-31 19:25:30 +00:00
vol.Required(ATTR_ENDPOINT_ID): int,
vol.Required(ATTR_CLUSTER_ID): int,
vol.Required(ATTR_CLUSTER_TYPE): str,
}
)
2019-03-01 00:32:41 +00:00
async def websocket_device_cluster_attributes(hass, connection, msg):
"""Return a list of cluster attributes."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
ieee = msg[ATTR_IEEE]
endpoint_id = msg[ATTR_ENDPOINT_ID]
cluster_id = msg[ATTR_CLUSTER_ID]
cluster_type = msg[ATTR_CLUSTER_TYPE]
cluster_attributes = []
zha_device = zha_gateway.get_device(ieee)
attributes = None
if zha_device is not None:
attributes = zha_device.async_get_cluster_attributes(
2019-07-31 19:25:30 +00:00
endpoint_id, cluster_id, cluster_type
)
2019-03-01 00:32:41 +00:00
if attributes is not None:
for attr_id in attributes:
cluster_attributes.append(
{ID: attr_id, ATTR_NAME: attributes[attr_id][0]}
)
2019-07-31 19:25:30 +00:00
_LOGGER.debug(
2019-12-12 19:16:02 +00:00
"Requested attributes for: %s: %s, %s: '%s', %s: %s, %s: %s",
ATTR_CLUSTER_ID,
cluster_id,
ATTR_CLUSTER_TYPE,
cluster_type,
ATTR_ENDPOINT_ID,
endpoint_id,
RESPONSE,
cluster_attributes,
2019-07-31 19:25:30 +00:00
)
2019-03-01 00:32:41 +00:00
connection.send_result(msg[ID], cluster_attributes)
@websocket_api.require_admin
2019-03-01 00:32:41 +00:00
@websocket_api.async_response
2019-07-31 19:25:30 +00:00
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zha/devices/clusters/commands",
vol.Required(ATTR_IEEE): EUI64.convert,
2019-07-31 19:25:30 +00:00
vol.Required(ATTR_ENDPOINT_ID): int,
vol.Required(ATTR_CLUSTER_ID): int,
vol.Required(ATTR_CLUSTER_TYPE): str,
}
)
2019-03-01 00:32:41 +00:00
async def websocket_device_cluster_commands(hass, connection, msg):
"""Return a list of cluster commands."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
cluster_id = msg[ATTR_CLUSTER_ID]
cluster_type = msg[ATTR_CLUSTER_TYPE]
ieee = msg[ATTR_IEEE]
endpoint_id = msg[ATTR_ENDPOINT_ID]
zha_device = zha_gateway.get_device(ieee)
cluster_commands = []
commands = None
if zha_device is not None:
commands = zha_device.async_get_cluster_commands(
2019-07-31 19:25:30 +00:00
endpoint_id, cluster_id, cluster_type
)
2019-03-01 00:32:41 +00:00
if commands is not None:
for cmd_id in commands[CLUSTER_COMMANDS_CLIENT]:
2019-03-01 00:32:41 +00:00
cluster_commands.append(
{
TYPE: CLIENT,
ID: cmd_id,
ATTR_NAME: commands[CLUSTER_COMMANDS_CLIENT][cmd_id][0],
2019-03-01 00:32:41 +00:00
}
)
for cmd_id in commands[CLUSTER_COMMANDS_SERVER]:
2019-03-01 00:32:41 +00:00
cluster_commands.append(
{
TYPE: CLUSTER_COMMAND_SERVER,
2019-03-01 00:32:41 +00:00
ID: cmd_id,
ATTR_NAME: commands[CLUSTER_COMMANDS_SERVER][cmd_id][0],
2019-03-01 00:32:41 +00:00
}
)
2019-07-31 19:25:30 +00:00
_LOGGER.debug(
2019-12-12 19:16:02 +00:00
"Requested commands for: %s: %s, %s: '%s', %s: %s, %s: %s",
ATTR_CLUSTER_ID,
cluster_id,
ATTR_CLUSTER_TYPE,
cluster_type,
ATTR_ENDPOINT_ID,
endpoint_id,
RESPONSE,
cluster_commands,
2019-07-31 19:25:30 +00:00
)
2019-03-01 00:32:41 +00:00
connection.send_result(msg[ID], cluster_commands)
@websocket_api.require_admin
2019-03-01 00:32:41 +00:00
@websocket_api.async_response
2019-07-31 19:25:30 +00:00
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zha/devices/clusters/attributes/value",
vol.Required(ATTR_IEEE): EUI64.convert,
2019-07-31 19:25:30 +00:00
vol.Required(ATTR_ENDPOINT_ID): int,
vol.Required(ATTR_CLUSTER_ID): int,
vol.Required(ATTR_CLUSTER_TYPE): str,
vol.Required(ATTR_ATTRIBUTE): int,
vol.Optional(ATTR_MANUFACTURER): object,
}
)
2019-03-01 00:32:41 +00:00
async def websocket_read_zigbee_cluster_attributes(hass, connection, msg):
"""Read zigbee attribute for cluster on zha entity."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
ieee = msg[ATTR_IEEE]
endpoint_id = msg[ATTR_ENDPOINT_ID]
cluster_id = msg[ATTR_CLUSTER_ID]
cluster_type = msg[ATTR_CLUSTER_TYPE]
attribute = msg[ATTR_ATTRIBUTE]
2019-03-26 13:17:43 +00:00
manufacturer = msg.get(ATTR_MANUFACTURER) or None
2019-03-01 00:32:41 +00:00
zha_device = zha_gateway.get_device(ieee)
2019-03-26 13:17:43 +00:00
if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None:
manufacturer = zha_device.manufacturer_code
2019-03-01 00:32:41 +00:00
success = failure = None
if zha_device is not None:
cluster = zha_device.async_get_cluster(
2019-07-31 19:25:30 +00:00
endpoint_id, cluster_id, cluster_type=cluster_type
)
2019-03-01 00:32:41 +00:00
success, failure = await cluster.read_attributes(
2019-07-31 19:25:30 +00:00
[attribute], allow_cache=False, only_cache=False, manufacturer=manufacturer
2019-03-01 00:32:41 +00:00
)
2019-07-31 19:25:30 +00:00
_LOGGER.debug(
2019-12-12 19:16:02 +00:00
"Read attribute for: %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s],",
ATTR_CLUSTER_ID,
cluster_id,
ATTR_CLUSTER_TYPE,
cluster_type,
ATTR_ENDPOINT_ID,
endpoint_id,
ATTR_ATTRIBUTE,
attribute,
ATTR_MANUFACTURER,
manufacturer,
RESPONSE,
str(success.get(attribute)),
"failure",
failure,
2019-07-31 19:25:30 +00:00
)
2019-03-01 00:32:41 +00:00
connection.send_result(msg[ID], str(success.get(attribute)))
@websocket_api.require_admin
2019-03-01 00:32:41 +00:00
@websocket_api.async_response
2019-07-31 19:25:30 +00:00
@websocket_api.websocket_command(
{vol.Required(TYPE): "zha/devices/bindable", vol.Required(ATTR_IEEE): EUI64.convert}
2019-07-31 19:25:30 +00:00
)
2019-03-01 00:32:41 +00:00
async def websocket_get_bindable_devices(hass, connection, msg):
"""Directly bind devices."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
source_ieee = msg[ATTR_IEEE]
source_device = zha_gateway.get_device(source_ieee)
2019-04-19 00:21:30 +00:00
ha_device_registry = await async_get_registry(hass)
2019-03-01 00:32:41 +00:00
devices = [
2019-07-31 19:25:30 +00:00
async_get_device_info(hass, device, ha_device_registry=ha_device_registry)
for device in zha_gateway.devices.values()
if async_is_bindable_target(source_device, device)
2019-03-01 00:32:41 +00:00
]
2019-07-31 19:25:30 +00:00
_LOGGER.debug(
2019-12-12 19:16:02 +00:00
"Get bindable devices: %s: [%s], %s: [%s]",
ATTR_SOURCE_IEEE,
source_ieee,
"bindable devices",
devices,
2019-07-31 19:25:30 +00:00
)
2019-03-01 00:32:41 +00:00
2019-07-31 19:25:30 +00:00
connection.send_message(websocket_api.result_message(msg[ID], devices))
2019-03-01 00:32:41 +00:00
@websocket_api.require_admin
2019-03-01 00:32:41 +00:00
@websocket_api.async_response
2019-07-31 19:25:30 +00:00
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zha/devices/bind",
vol.Required(ATTR_SOURCE_IEEE): EUI64.convert,
vol.Required(ATTR_TARGET_IEEE): EUI64.convert,
2019-07-31 19:25:30 +00:00
}
)
2019-03-01 00:32:41 +00:00
async def websocket_bind_devices(hass, connection, msg):
"""Directly bind devices."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
source_ieee = msg[ATTR_SOURCE_IEEE]
target_ieee = msg[ATTR_TARGET_IEEE]
await async_binding_operation(
zha_gateway, source_ieee, target_ieee, zdo_types.ZDOCmd.Bind_req
)
2019-07-31 19:25:30 +00:00
_LOGGER.info(
2019-12-12 19:16:02 +00:00
"Devices bound: %s: [%s] %s: [%s]",
ATTR_SOURCE_IEEE,
source_ieee,
ATTR_TARGET_IEEE,
target_ieee,
2019-07-31 19:25:30 +00:00
)
2019-03-01 00:32:41 +00:00
@websocket_api.require_admin
2019-03-01 00:32:41 +00:00
@websocket_api.async_response
2019-07-31 19:25:30 +00:00
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zha/devices/unbind",
vol.Required(ATTR_SOURCE_IEEE): EUI64.convert,
vol.Required(ATTR_TARGET_IEEE): EUI64.convert,
2019-07-31 19:25:30 +00:00
}
)
2019-03-01 00:32:41 +00:00
async def websocket_unbind_devices(hass, connection, msg):
"""Remove a direct binding between devices."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
source_ieee = msg[ATTR_SOURCE_IEEE]
target_ieee = msg[ATTR_TARGET_IEEE]
await async_binding_operation(
zha_gateway, source_ieee, target_ieee, zdo_types.ZDOCmd.Unbind_req
)
2019-07-31 19:25:30 +00:00
_LOGGER.info(
2019-12-12 19:16:02 +00:00
"Devices un-bound: %s: [%s] %s: [%s]",
ATTR_SOURCE_IEEE,
source_ieee,
ATTR_TARGET_IEEE,
target_ieee,
2019-07-31 19:25:30 +00:00
)
2019-03-01 00:32:41 +00:00
2019-07-31 19:25:30 +00:00
async def async_binding_operation(zha_gateway, source_ieee, target_ieee, operation):
2019-03-01 00:32:41 +00:00
"""Create or remove a direct zigbee binding between 2 devices."""
2019-07-31 19:25:30 +00:00
2019-03-01 00:32:41 +00:00
source_device = zha_gateway.get_device(source_ieee)
target_device = zha_gateway.get_device(target_ieee)
2019-07-31 19:25:30 +00:00
clusters_to_bind = await get_matched_clusters(source_device, target_device)
2019-03-01 00:32:41 +00:00
bind_tasks = []
for cluster_pair in clusters_to_bind:
destination_address = zdo_types.MultiAddress()
destination_address.addrmode = 3
destination_address.ieee = target_device.ieee
2019-07-31 19:25:30 +00:00
destination_address.endpoint = cluster_pair.target_cluster.endpoint.endpoint_id
2019-03-01 00:32:41 +00:00
zdo = cluster_pair.source_cluster.endpoint.device.zdo
op_msg = "cluster: %s %s --> [%s]"
op_params = (
cluster_pair.source_cluster.cluster_id,
operation.name,
target_ieee,
2019-07-31 19:25:30 +00:00
)
zdo.debug("processing " + op_msg, *op_params)
2019-07-31 19:25:30 +00:00
bind_tasks.append(
(
zdo.request(
operation,
source_device.ieee,
cluster_pair.source_cluster.endpoint.endpoint_id,
cluster_pair.source_cluster.cluster_id,
destination_address,
),
op_msg,
op_params,
2019-07-31 19:25:30 +00:00
)
)
res = await asyncio.gather(*(t[0] for t in bind_tasks), return_exceptions=True)
for outcome, log_msg in zip(res, bind_tasks):
if isinstance(outcome, Exception):
fmt = log_msg[1] + " failed: %s"
else:
fmt = log_msg[1] + " completed: %s"
zdo.debug(fmt, *(log_msg[2] + (outcome,)))
def async_load_api(hass):
"""Set up the web socket API."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
application_controller = zha_gateway.application_controller
async def permit(service):
"""Allow devices to join this network."""
duration = service.data.get(ATTR_DURATION)
ieee = service.data.get(ATTR_IEEE_ADDRESS)
if ieee:
2019-07-31 19:25:30 +00:00
_LOGGER.info("Permitting joins for %ss on %s device", duration, ieee)
else:
_LOGGER.info("Permitting joins for %ss", duration)
await application_controller.permit(time_s=duration, node=ieee)
hass.helpers.service.async_register_admin_service(
2019-07-31 19:25:30 +00:00
DOMAIN, SERVICE_PERMIT, permit, schema=SERVICE_SCHEMAS[SERVICE_PERMIT]
)
async def remove(service):
"""Remove a node from the network."""
ieee = service.data.get(ATTR_IEEE_ADDRESS)
_LOGGER.info("Removing node %s", ieee)
await application_controller.remove(ieee)
hass.helpers.service.async_register_admin_service(
2019-07-31 19:25:30 +00:00
DOMAIN, SERVICE_REMOVE, remove, schema=SERVICE_SCHEMAS[IEEE_SERVICE]
)
async def set_zigbee_cluster_attributes(service):
"""Set zigbee attribute for cluster on zha entity."""
ieee = service.data.get(ATTR_IEEE)
endpoint_id = service.data.get(ATTR_ENDPOINT_ID)
cluster_id = service.data.get(ATTR_CLUSTER_ID)
cluster_type = service.data.get(ATTR_CLUSTER_TYPE)
attribute = service.data.get(ATTR_ATTRIBUTE)
value = service.data.get(ATTR_VALUE)
2019-03-26 13:17:43 +00:00
manufacturer = service.data.get(ATTR_MANUFACTURER) or None
zha_device = zha_gateway.get_device(ieee)
2019-03-26 13:17:43 +00:00
if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None:
manufacturer = zha_device.manufacturer_code
response = None
if zha_device is not None:
response = await zha_device.write_zigbee_attribute(
endpoint_id,
cluster_id,
attribute,
value,
cluster_type=cluster_type,
2019-07-31 19:25:30 +00:00
manufacturer=manufacturer,
)
2019-07-31 19:25:30 +00:00
_LOGGER.debug(
2019-12-12 19:16:02 +00:00
"Set attribute for: %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s]",
ATTR_CLUSTER_ID,
cluster_id,
ATTR_CLUSTER_TYPE,
cluster_type,
ATTR_ENDPOINT_ID,
endpoint_id,
ATTR_ATTRIBUTE,
attribute,
ATTR_VALUE,
value,
ATTR_MANUFACTURER,
manufacturer,
RESPONSE,
response,
2019-07-31 19:25:30 +00:00
)
hass.helpers.service.async_register_admin_service(
2019-07-31 19:25:30 +00:00
DOMAIN,
SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE,
set_zigbee_cluster_attributes,
2019-07-31 19:25:30 +00:00
schema=SERVICE_SCHEMAS[SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE],
)
async def issue_zigbee_cluster_command(service):
"""Issue command on zigbee cluster on zha entity."""
ieee = service.data.get(ATTR_IEEE)
endpoint_id = service.data.get(ATTR_ENDPOINT_ID)
cluster_id = service.data.get(ATTR_CLUSTER_ID)
cluster_type = service.data.get(ATTR_CLUSTER_TYPE)
command = service.data.get(ATTR_COMMAND)
command_type = service.data.get(ATTR_COMMAND_TYPE)
args = service.data.get(ATTR_ARGS)
2019-03-26 13:17:43 +00:00
manufacturer = service.data.get(ATTR_MANUFACTURER) or None
zha_device = zha_gateway.get_device(ieee)
2019-03-26 13:17:43 +00:00
if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None:
manufacturer = zha_device.manufacturer_code
response = None
if zha_device is not None:
response = await zha_device.issue_cluster_command(
endpoint_id,
cluster_id,
command,
command_type,
*args,
cluster_type=cluster_type,
2019-07-31 19:25:30 +00:00
manufacturer=manufacturer,
)
2019-07-31 19:25:30 +00:00
_LOGGER.debug(
2019-12-12 19:16:02 +00:00
"Issued command for: %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: %s %s: [%s] %s: %s",
ATTR_CLUSTER_ID,
cluster_id,
ATTR_CLUSTER_TYPE,
cluster_type,
ATTR_ENDPOINT_ID,
endpoint_id,
ATTR_COMMAND,
command,
ATTR_COMMAND_TYPE,
command_type,
ATTR_ARGS,
args,
ATTR_MANUFACTURER,
manufacturer,
RESPONSE,
response,
2019-07-31 19:25:30 +00:00
)
hass.helpers.service.async_register_admin_service(
2019-07-31 19:25:30 +00:00
DOMAIN,
SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND,
issue_zigbee_cluster_command,
2019-07-31 19:25:30 +00:00
schema=SERVICE_SCHEMAS[SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND],
)
async def issue_zigbee_group_command(service):
"""Issue command on zigbee cluster on a zigbee group."""
group_id = service.data.get(ATTR_GROUP)
cluster_id = service.data.get(ATTR_CLUSTER_ID)
command = service.data.get(ATTR_COMMAND)
args = service.data.get(ATTR_ARGS)
manufacturer = service.data.get(ATTR_MANUFACTURER) or None
group = zha_gateway.get_group(group_id)
if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None:
_LOGGER.error("Missing manufacturer attribute for cluster: %d", cluster_id)
response = None
if group is not None:
cluster = group.endpoint[cluster_id]
response = await cluster.command(
command, *args, manufacturer=manufacturer, expect_reply=True
)
_LOGGER.debug(
2019-12-12 19:16:02 +00:00
"Issued group command for: %s: [%s] %s: [%s] %s: %s %s: [%s] %s: %s",
ATTR_CLUSTER_ID,
cluster_id,
ATTR_COMMAND,
command,
ATTR_ARGS,
args,
ATTR_MANUFACTURER,
manufacturer,
RESPONSE,
response,
)
hass.helpers.service.async_register_admin_service(
DOMAIN,
SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND,
issue_zigbee_group_command,
schema=SERVICE_SCHEMAS[SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND],
)
async def warning_device_squawk(service):
"""Issue the squawk command for an IAS warning device."""
ieee = service.data[ATTR_IEEE]
mode = service.data.get(ATTR_WARNING_DEVICE_MODE)
strobe = service.data.get(ATTR_WARNING_DEVICE_STROBE)
level = service.data.get(ATTR_LEVEL)
zha_device = zha_gateway.get_device(ieee)
if zha_device is not None:
channel = zha_device.cluster_channels.get(CHANNEL_IAS_WD)
if channel:
await channel.squawk(mode, strobe, level)
else:
_LOGGER.error(
2019-12-12 19:16:02 +00:00
"Squawking IASWD: %s: [%s] is missing the required IASWD channel!",
ATTR_IEEE,
str(ieee),
)
else:
_LOGGER.error(
2019-12-12 19:16:02 +00:00
"Squawking IASWD: %s: [%s] could not be found!", ATTR_IEEE, str(ieee)
)
_LOGGER.debug(
2019-12-12 19:16:02 +00:00
"Squawking IASWD: %s: [%s] %s: [%s] %s: [%s] %s: [%s]",
ATTR_IEEE,
str(ieee),
ATTR_WARNING_DEVICE_MODE,
mode,
ATTR_WARNING_DEVICE_STROBE,
strobe,
ATTR_LEVEL,
level,
)
hass.helpers.service.async_register_admin_service(
DOMAIN,
SERVICE_WARNING_DEVICE_SQUAWK,
warning_device_squawk,
schema=SERVICE_SCHEMAS[SERVICE_WARNING_DEVICE_SQUAWK],
)
async def warning_device_warn(service):
"""Issue the warning command for an IAS warning device."""
ieee = service.data[ATTR_IEEE]
mode = service.data.get(ATTR_WARNING_DEVICE_MODE)
strobe = service.data.get(ATTR_WARNING_DEVICE_STROBE)
level = service.data.get(ATTR_LEVEL)
duration = service.data.get(ATTR_WARNING_DEVICE_DURATION)
duty_mode = service.data.get(ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE)
intensity = service.data.get(ATTR_WARNING_DEVICE_STROBE_INTENSITY)
zha_device = zha_gateway.get_device(ieee)
if zha_device is not None:
channel = zha_device.cluster_channels.get(CHANNEL_IAS_WD)
if channel:
await channel.start_warning(
mode, strobe, level, duration, duty_mode, intensity
)
else:
_LOGGER.error(
2019-12-12 19:16:02 +00:00
"Warning IASWD: %s: [%s] is missing the required IASWD channel!",
ATTR_IEEE,
str(ieee),
)
else:
_LOGGER.error(
2019-12-12 19:16:02 +00:00
"Warning IASWD: %s: [%s] could not be found!", ATTR_IEEE, str(ieee)
)
_LOGGER.debug(
2019-12-12 19:16:02 +00:00
"Warning IASWD: %s: [%s] %s: [%s] %s: [%s] %s: [%s]",
ATTR_IEEE,
str(ieee),
ATTR_WARNING_DEVICE_MODE,
mode,
ATTR_WARNING_DEVICE_STROBE,
strobe,
ATTR_LEVEL,
level,
)
hass.helpers.service.async_register_admin_service(
DOMAIN,
SERVICE_WARNING_DEVICE_WARN,
warning_device_warn,
schema=SERVICE_SCHEMAS[SERVICE_WARNING_DEVICE_WARN],
)
websocket_api.async_register_command(hass, websocket_permit_devices)
2019-03-01 00:32:41 +00:00
websocket_api.async_register_command(hass, websocket_get_devices)
websocket_api.async_register_command(hass, websocket_get_groupable_devices)
websocket_api.async_register_command(hass, websocket_get_groups)
websocket_api.async_register_command(hass, websocket_get_device)
websocket_api.async_register_command(hass, websocket_get_group)
websocket_api.async_register_command(hass, websocket_add_group)
websocket_api.async_register_command(hass, websocket_remove_groups)
websocket_api.async_register_command(hass, websocket_add_group_members)
websocket_api.async_register_command(hass, websocket_remove_group_members)
2019-03-01 00:32:41 +00:00
websocket_api.async_register_command(hass, websocket_reconfigure_node)
websocket_api.async_register_command(hass, websocket_device_clusters)
2019-07-31 19:25:30 +00:00
websocket_api.async_register_command(hass, websocket_device_cluster_attributes)
websocket_api.async_register_command(hass, websocket_device_cluster_commands)
websocket_api.async_register_command(hass, websocket_read_zigbee_cluster_attributes)
2019-03-01 00:32:41 +00:00
websocket_api.async_register_command(hass, websocket_get_bindable_devices)
websocket_api.async_register_command(hass, websocket_bind_devices)
websocket_api.async_register_command(hass, websocket_unbind_devices)
def async_unload_api(hass):
"""Unload the ZHA API."""
hass.services.async_remove(DOMAIN, SERVICE_PERMIT)
hass.services.async_remove(DOMAIN, SERVICE_REMOVE)
hass.services.async_remove(DOMAIN, SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE)
hass.services.async_remove(DOMAIN, SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND)
hass.services.async_remove(DOMAIN, SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND)
hass.services.async_remove(DOMAIN, SERVICE_WARNING_DEVICE_SQUAWK)
hass.services.async_remove(DOMAIN, SERVICE_WARNING_DEVICE_WARN)