Add services and helper functions to support a config panel for ZHA (#19664)
* reconfigure zha device service add log line to reconfigure service for consistency * add entity functions to support new services * added new services and web socket api and split them into their own module * support manufacturer code logging to debug get safe value for manufacturer * update services.yaml * add comma back * update coveragerc * remove blank line * fix type * api cleanup - review comments * move static method to helpers - review comment * convert reconfigure service to websocket command - review comment * change path * fix attribute
This commit is contained in:
parent
a8f22287ca
commit
7be015fcc6
7 changed files with 637 additions and 45 deletions
|
@ -426,6 +426,7 @@ omit =
|
|||
homeassistant/components/zha/__init__.py
|
||||
homeassistant/components/zha/const.py
|
||||
homeassistant/components/zha/event.py
|
||||
homeassistant/components/zha/api.py
|
||||
homeassistant/components/zha/entities/*
|
||||
homeassistant/components/zha/helpers.py
|
||||
homeassistant/components/*/zha.py
|
||||
|
|
|
@ -12,6 +12,7 @@ import types
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, const as ha_const
|
||||
|
||||
from homeassistant.components.zha.entities import ZhaDeviceEntity
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
|
||||
|
@ -22,6 +23,8 @@ from homeassistant.helpers.entity_component import EntityComponent
|
|||
from . import config_flow # noqa # pylint: disable=unused-import
|
||||
from . import const as zha_const
|
||||
from .event import ZhaEvent, ZhaRelayEvent
|
||||
from . import api
|
||||
from .helpers import convert_ieee
|
||||
from .const import (
|
||||
COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG,
|
||||
CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, DATA_ZHA_BRIDGE_ID,
|
||||
|
@ -56,22 +59,6 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
ATTR_DURATION = 'duration'
|
||||
ATTR_IEEE = 'ieee_address'
|
||||
|
||||
SERVICE_PERMIT = 'permit'
|
||||
SERVICE_REMOVE = 'remove'
|
||||
SERVICE_SCHEMAS = {
|
||||
SERVICE_PERMIT: vol.Schema({
|
||||
vol.Optional(ATTR_DURATION, default=60):
|
||||
vol.All(vol.Coerce(int), vol.Range(1, 254)),
|
||||
}),
|
||||
SERVICE_REMOVE: vol.Schema({
|
||||
vol.Required(ATTR_IEEE): cv.string,
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
# Zigbee definitions
|
||||
CENTICELSIUS = 'C-100'
|
||||
|
||||
|
@ -179,25 +166,7 @@ async def async_setup_entry(hass, config_entry):
|
|||
config_entry, component)
|
||||
)
|
||||
|
||||
async def permit(service):
|
||||
"""Allow devices to join this network."""
|
||||
duration = service.data.get(ATTR_DURATION)
|
||||
_LOGGER.info("Permitting joins for %ss", duration)
|
||||
await application_controller.permit(duration)
|
||||
|
||||
hass.services.async_register(DOMAIN, SERVICE_PERMIT, permit,
|
||||
schema=SERVICE_SCHEMAS[SERVICE_PERMIT])
|
||||
|
||||
async def remove(service):
|
||||
"""Remove a node from the network."""
|
||||
from bellows.types import EmberEUI64, uint8_t
|
||||
ieee = service.data.get(ATTR_IEEE)
|
||||
ieee = EmberEUI64([uint8_t(p, base=16) for p in ieee.split(':')])
|
||||
_LOGGER.info("Removing node %s", ieee)
|
||||
await application_controller.remove(ieee)
|
||||
|
||||
hass.services.async_register(DOMAIN, SERVICE_REMOVE, remove,
|
||||
schema=SERVICE_SCHEMAS[SERVICE_REMOVE])
|
||||
api.async_load_api(hass, application_controller, listener)
|
||||
|
||||
def zha_shutdown(event):
|
||||
"""Close radio."""
|
||||
|
@ -209,8 +178,7 @@ async def async_setup_entry(hass, config_entry):
|
|||
|
||||
async def async_unload_entry(hass, config_entry):
|
||||
"""Unload ZHA config entry."""
|
||||
hass.services.async_remove(DOMAIN, SERVICE_PERMIT)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_REMOVE)
|
||||
api.async_unload_api(hass)
|
||||
|
||||
dispatchers = hass.data[DATA_ZHA].get(DATA_ZHA_DISPATCHERS, [])
|
||||
for unsub_dispatcher in dispatchers:
|
||||
|
@ -285,6 +253,28 @@ class ApplicationListener:
|
|||
if device.ieee in self._events:
|
||||
self._events.pop(device.ieee)
|
||||
|
||||
def get_device_entity(self, ieee_str):
|
||||
"""Return ZHADeviceEntity for given ieee."""
|
||||
ieee = convert_ieee(ieee_str)
|
||||
if ieee in self._device_registry:
|
||||
entities = self._device_registry[ieee]
|
||||
entity = next(
|
||||
ent for ent in entities if isinstance(ent, ZhaDeviceEntity))
|
||||
return entity
|
||||
return None
|
||||
|
||||
def get_entities_for_ieee(self, ieee_str):
|
||||
"""Return list of entities for given ieee."""
|
||||
ieee = convert_ieee(ieee_str)
|
||||
if ieee in self._device_registry:
|
||||
return self._device_registry[ieee]
|
||||
return []
|
||||
|
||||
@property
|
||||
def device_registry(self) -> str:
|
||||
"""Return devices."""
|
||||
return self._device_registry
|
||||
|
||||
async def async_device_initialized(self, device, join):
|
||||
"""Handle device joined and basic information discovered (async)."""
|
||||
import zigpy.profiles
|
||||
|
|
416
homeassistant/components/zha/api.py
Normal file
416
homeassistant/components/zha/api.py
Normal file
|
@ -0,0 +1,416 @@
|
|||
"""
|
||||
Web socket API for Zigbee Home Automation devices.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/zha/
|
||||
"""
|
||||
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.zha.entities import ZhaDeviceEntity
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from .const import (
|
||||
DOMAIN, ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, ATTR_ATTRIBUTE, ATTR_VALUE,
|
||||
ATTR_MANUFACTURER, ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ARGS, IN, OUT,
|
||||
CLIENT_COMMANDS, SERVER_COMMANDS, SERVER)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TYPE = 'type'
|
||||
CLIENT = 'client'
|
||||
ID = 'id'
|
||||
NAME = 'name'
|
||||
RESPONSE = 'response'
|
||||
DEVICE_INFO = 'device_info'
|
||||
|
||||
ATTR_DURATION = 'duration'
|
||||
ATTR_IEEE_ADDRESS = 'ieee_address'
|
||||
ATTR_IEEE = 'ieee'
|
||||
|
||||
SERVICE_PERMIT = 'permit'
|
||||
SERVICE_REMOVE = 'remove'
|
||||
SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE = 'set_zigbee_cluster_attribute'
|
||||
SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND = 'issue_zigbee_cluster_command'
|
||||
ZIGBEE_CLUSTER_SERVICE = 'zigbee_cluster_service'
|
||||
IEEE_SERVICE = 'ieee_based_service'
|
||||
|
||||
SERVICE_SCHEMAS = {
|
||||
SERVICE_PERMIT: vol.Schema({
|
||||
vol.Optional(ATTR_DURATION, default=60):
|
||||
vol.All(vol.Coerce(int), vol.Range(1, 254)),
|
||||
}),
|
||||
IEEE_SERVICE: vol.Schema({
|
||||
vol.Required(ATTR_IEEE_ADDRESS): cv.string,
|
||||
}),
|
||||
ZIGBEE_CLUSTER_SERVICE: vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(ATTR_CLUSTER_ID): cv.positive_int,
|
||||
vol.Optional(ATTR_CLUSTER_TYPE, default=IN): cv.string
|
||||
}),
|
||||
SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE: vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(ATTR_CLUSTER_ID): cv.positive_int,
|
||||
vol.Optional(ATTR_CLUSTER_TYPE, default=IN): cv.string,
|
||||
vol.Required(ATTR_ATTRIBUTE): cv.positive_int,
|
||||
vol.Required(ATTR_VALUE): cv.string,
|
||||
vol.Optional(ATTR_MANUFACTURER): cv.positive_int,
|
||||
}),
|
||||
SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND: vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(ATTR_CLUSTER_ID): cv.positive_int,
|
||||
vol.Optional(ATTR_CLUSTER_TYPE, default=IN): cv.string,
|
||||
vol.Required(ATTR_COMMAND): cv.positive_int,
|
||||
vol.Required(ATTR_COMMAND_TYPE): cv.string,
|
||||
vol.Optional(ATTR_ARGS, default=''): cv.string,
|
||||
vol.Optional(ATTR_MANUFACTURER): cv.positive_int,
|
||||
}),
|
||||
}
|
||||
|
||||
WS_RECONFIGURE_NODE = 'zha/nodes/reconfigure'
|
||||
SCHEMA_WS_RECONFIGURE_NODE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required(TYPE): WS_RECONFIGURE_NODE,
|
||||
vol.Required(ATTR_IEEE): str
|
||||
})
|
||||
|
||||
WS_ENTITIES_BY_IEEE = 'zha/entities'
|
||||
SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required(TYPE): WS_ENTITIES_BY_IEEE,
|
||||
})
|
||||
|
||||
WS_ENTITY_CLUSTERS = 'zha/entities/clusters'
|
||||
SCHEMA_WS_CLUSTERS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required(TYPE): WS_ENTITY_CLUSTERS,
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(ATTR_IEEE): str
|
||||
})
|
||||
|
||||
WS_ENTITY_CLUSTER_ATTRIBUTES = 'zha/entities/clusters/attributes'
|
||||
SCHEMA_WS_CLUSTER_ATTRIBUTES = \
|
||||
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required(TYPE): WS_ENTITY_CLUSTER_ATTRIBUTES,
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(ATTR_IEEE): str,
|
||||
vol.Required(ATTR_CLUSTER_ID): int,
|
||||
vol.Required(ATTR_CLUSTER_TYPE): str
|
||||
})
|
||||
|
||||
WS_READ_CLUSTER_ATTRIBUTE = 'zha/entities/clusters/attributes/value'
|
||||
SCHEMA_WS_READ_CLUSTER_ATTRIBUTE = \
|
||||
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required(TYPE): WS_READ_CLUSTER_ATTRIBUTE,
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(ATTR_CLUSTER_ID): int,
|
||||
vol.Required(ATTR_CLUSTER_TYPE): str,
|
||||
vol.Required(ATTR_ATTRIBUTE): int,
|
||||
vol.Optional(ATTR_MANUFACTURER): object,
|
||||
})
|
||||
|
||||
WS_ENTITY_CLUSTER_COMMANDS = 'zha/entities/clusters/commands'
|
||||
SCHEMA_WS_CLUSTER_COMMANDS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required(TYPE): WS_ENTITY_CLUSTER_COMMANDS,
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(ATTR_IEEE): str,
|
||||
vol.Required(ATTR_CLUSTER_ID): int,
|
||||
vol.Required(ATTR_CLUSTER_TYPE): str
|
||||
})
|
||||
|
||||
|
||||
@websocket_api.async_response
|
||||
async def websocket_entity_cluster_attributes(hass, connection, msg):
|
||||
"""Return a list of cluster attributes."""
|
||||
entity_id = msg[ATTR_ENTITY_ID]
|
||||
cluster_id = msg[ATTR_CLUSTER_ID]
|
||||
cluster_type = msg[ATTR_CLUSTER_TYPE]
|
||||
component = hass.data.get(entity_id.split('.')[0])
|
||||
entity = component.get_entity(entity_id)
|
||||
cluster_attributes = []
|
||||
if entity is not None:
|
||||
res = await entity.get_cluster_attributes(cluster_id, cluster_type)
|
||||
if res is not None:
|
||||
for attr_id in res:
|
||||
cluster_attributes.append(
|
||||
{
|
||||
ID: attr_id,
|
||||
NAME: res[attr_id][0]
|
||||
}
|
||||
)
|
||||
_LOGGER.debug("Requested attributes for: %s %s %s %s",
|
||||
"{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id),
|
||||
"{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type),
|
||||
"{}: [{}]".format(ATTR_ENTITY_ID, entity_id),
|
||||
"{}: [{}]".format(RESPONSE, cluster_attributes)
|
||||
)
|
||||
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg[ID],
|
||||
cluster_attributes
|
||||
))
|
||||
|
||||
|
||||
@websocket_api.async_response
|
||||
async def websocket_entity_cluster_commands(hass, connection, msg):
|
||||
"""Return a list of cluster commands."""
|
||||
entity_id = msg[ATTR_ENTITY_ID]
|
||||
cluster_id = msg[ATTR_CLUSTER_ID]
|
||||
cluster_type = msg[ATTR_CLUSTER_TYPE]
|
||||
component = hass.data.get(entity_id.split('.')[0])
|
||||
entity = component.get_entity(entity_id)
|
||||
cluster_commands = []
|
||||
if entity is not None:
|
||||
res = await entity.get_cluster_commands(cluster_id, cluster_type)
|
||||
if res is not None:
|
||||
for cmd_id in res[CLIENT_COMMANDS]:
|
||||
cluster_commands.append(
|
||||
{
|
||||
TYPE: CLIENT,
|
||||
ID: cmd_id,
|
||||
NAME: res[CLIENT_COMMANDS][cmd_id][0]
|
||||
}
|
||||
)
|
||||
for cmd_id in res[SERVER_COMMANDS]:
|
||||
cluster_commands.append(
|
||||
{
|
||||
TYPE: SERVER,
|
||||
ID: cmd_id,
|
||||
NAME: res[SERVER_COMMANDS][cmd_id][0]
|
||||
}
|
||||
)
|
||||
_LOGGER.debug("Requested commands for: %s %s %s %s",
|
||||
"{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id),
|
||||
"{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type),
|
||||
"{}: [{}]".format(ATTR_ENTITY_ID, entity_id),
|
||||
"{}: [{}]".format(RESPONSE, cluster_commands)
|
||||
)
|
||||
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg[ID],
|
||||
cluster_commands
|
||||
))
|
||||
|
||||
|
||||
@websocket_api.async_response
|
||||
async def websocket_read_zigbee_cluster_attributes(hass, connection, msg):
|
||||
"""Read zigbee attribute for cluster on zha entity."""
|
||||
entity_id = msg[ATTR_ENTITY_ID]
|
||||
cluster_id = msg[ATTR_CLUSTER_ID]
|
||||
cluster_type = msg[ATTR_CLUSTER_TYPE]
|
||||
attribute = msg[ATTR_ATTRIBUTE]
|
||||
component = hass.data.get(entity_id.split('.')[0])
|
||||
entity = component.get_entity(entity_id)
|
||||
clusters = await entity.get_clusters()
|
||||
cluster = clusters[cluster_type][cluster_id]
|
||||
manufacturer = msg.get(ATTR_MANUFACTURER) or None
|
||||
success = failure = None
|
||||
if entity is not None:
|
||||
success, failure = await cluster.read_attributes(
|
||||
[attribute],
|
||||
allow_cache=False,
|
||||
only_cache=False,
|
||||
manufacturer=manufacturer
|
||||
)
|
||||
_LOGGER.debug("Read attribute for: %s %s %s %s %s %s %s",
|
||||
"{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id),
|
||||
"{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type),
|
||||
"{}: [{}]".format(ATTR_ENTITY_ID, entity_id),
|
||||
"{}: [{}]".format(ATTR_ATTRIBUTE, attribute),
|
||||
"{}: [{}]".format(ATTR_MANUFACTURER, manufacturer),
|
||||
"{}: [{}]".format(RESPONSE, str(success.get(attribute))),
|
||||
"{}: [{}]".format('failure', failure)
|
||||
)
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg[ID],
|
||||
str(success.get(attribute))
|
||||
))
|
||||
|
||||
|
||||
def async_load_api(hass, application_controller, listener):
|
||||
"""Set up the web socket API."""
|
||||
async def permit(service):
|
||||
"""Allow devices to join this network."""
|
||||
duration = service.data.get(ATTR_DURATION)
|
||||
_LOGGER.info("Permitting joins for %ss", duration)
|
||||
await application_controller.permit(duration)
|
||||
|
||||
hass.services.async_register(DOMAIN, SERVICE_PERMIT, permit,
|
||||
schema=SERVICE_SCHEMAS[SERVICE_PERMIT])
|
||||
|
||||
async def remove(service):
|
||||
"""Remove a node from the network."""
|
||||
from bellows.types import EmberEUI64, uint8_t
|
||||
ieee = service.data.get(ATTR_IEEE_ADDRESS)
|
||||
ieee = EmberEUI64([uint8_t(p, base=16) for p in ieee.split(':')])
|
||||
_LOGGER.info("Removing node %s", ieee)
|
||||
await application_controller.remove(ieee)
|
||||
|
||||
hass.services.async_register(DOMAIN, SERVICE_REMOVE, remove,
|
||||
schema=SERVICE_SCHEMAS[IEEE_SERVICE])
|
||||
|
||||
async def set_zigbee_cluster_attributes(service):
|
||||
"""Set zigbee attribute for cluster on zha entity."""
|
||||
entity_id = service.data.get(ATTR_ENTITY_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)
|
||||
manufacturer = service.data.get(ATTR_MANUFACTURER) or None
|
||||
component = hass.data.get(entity_id.split('.')[0])
|
||||
entity = component.get_entity(entity_id)
|
||||
response = None
|
||||
if entity is not None:
|
||||
response = await entity.write_zigbe_attribute(
|
||||
cluster_id,
|
||||
attribute,
|
||||
value,
|
||||
cluster_type=cluster_type,
|
||||
manufacturer=manufacturer
|
||||
)
|
||||
_LOGGER.debug("Set attribute for: %s %s %s %s %s %s %s",
|
||||
"{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id),
|
||||
"{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type),
|
||||
"{}: [{}]".format(ATTR_ENTITY_ID, entity_id),
|
||||
"{}: [{}]".format(ATTR_ATTRIBUTE, attribute),
|
||||
"{}: [{}]".format(ATTR_VALUE, value),
|
||||
"{}: [{}]".format(ATTR_MANUFACTURER, manufacturer),
|
||||
"{}: [{}]".format(RESPONSE, response)
|
||||
)
|
||||
|
||||
hass.services.async_register(DOMAIN, SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE,
|
||||
set_zigbee_cluster_attributes,
|
||||
schema=SERVICE_SCHEMAS[
|
||||
SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE
|
||||
])
|
||||
|
||||
async def issue_zigbee_cluster_command(service):
|
||||
"""Issue command on zigbee cluster on zha entity."""
|
||||
entity_id = service.data.get(ATTR_ENTITY_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)
|
||||
manufacturer = service.data.get(ATTR_MANUFACTURER) or None
|
||||
component = hass.data.get(entity_id.split('.')[0])
|
||||
entity = component.get_entity(entity_id)
|
||||
response = None
|
||||
if entity is not None:
|
||||
response = await entity.issue_cluster_command(
|
||||
cluster_id,
|
||||
command,
|
||||
command_type,
|
||||
args,
|
||||
cluster_type=cluster_type,
|
||||
manufacturer=manufacturer
|
||||
)
|
||||
_LOGGER.debug("Issue command for: %s %s %s %s %s %s %s %s",
|
||||
"{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id),
|
||||
"{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type),
|
||||
"{}: [{}]".format(ATTR_ENTITY_ID, entity_id),
|
||||
"{}: [{}]".format(ATTR_COMMAND, command),
|
||||
"{}: [{}]".format(ATTR_COMMAND_TYPE, command_type),
|
||||
"{}: [{}]".format(ATTR_ARGS, args),
|
||||
"{}: [{}]".format(ATTR_MANUFACTURER, manufacturer),
|
||||
"{}: [{}]".format(RESPONSE, response)
|
||||
)
|
||||
|
||||
hass.services.async_register(DOMAIN, SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND,
|
||||
issue_zigbee_cluster_command,
|
||||
schema=SERVICE_SCHEMAS[
|
||||
SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND
|
||||
])
|
||||
|
||||
@websocket_api.async_response
|
||||
async def websocket_reconfigure_node(hass, connection, msg):
|
||||
"""Reconfigure a ZHA nodes entities by its ieee address."""
|
||||
ieee = msg[ATTR_IEEE]
|
||||
entities = listener.get_entities_for_ieee(ieee)
|
||||
_LOGGER.debug("Reconfiguring node with ieee_address: %s", ieee)
|
||||
for entity in entities:
|
||||
if hasattr(entity, 'async_configure'):
|
||||
hass.async_create_task(entity.async_configure())
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_RECONFIGURE_NODE, websocket_reconfigure_node,
|
||||
SCHEMA_WS_RECONFIGURE_NODE
|
||||
)
|
||||
|
||||
@websocket_api.async_response
|
||||
async def websocket_entities_by_ieee(hass, connection, msg):
|
||||
"""Return a dict of all zha entities grouped by ieee."""
|
||||
entities_by_ieee = {}
|
||||
for ieee, entities in listener.device_registry.items():
|
||||
ieee_string = str(ieee)
|
||||
entities_by_ieee[ieee_string] = []
|
||||
for entity in entities:
|
||||
if not isinstance(entity, ZhaDeviceEntity):
|
||||
entities_by_ieee[ieee_string].append({
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
DEVICE_INFO: entity.device_info
|
||||
})
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg[ID],
|
||||
entities_by_ieee
|
||||
))
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_ENTITIES_BY_IEEE, websocket_entities_by_ieee,
|
||||
SCHEMA_WS_LIST
|
||||
)
|
||||
|
||||
@websocket_api.async_response
|
||||
async def websocket_entity_clusters(hass, connection, msg):
|
||||
"""Return a list of entity clusters."""
|
||||
entity_id = msg[ATTR_ENTITY_ID]
|
||||
entities = listener.get_entities_for_ieee(msg[ATTR_IEEE])
|
||||
entity = next(
|
||||
ent for ent in entities if ent.entity_id == entity_id)
|
||||
entity_clusters = await entity.get_clusters()
|
||||
clusters = []
|
||||
|
||||
for cluster_id, cluster in entity_clusters[IN].items():
|
||||
clusters.append({
|
||||
TYPE: IN,
|
||||
ID: cluster_id,
|
||||
NAME: cluster.__class__.__name__
|
||||
})
|
||||
for cluster_id, cluster in entity_clusters[OUT].items():
|
||||
clusters.append({
|
||||
TYPE: OUT,
|
||||
ID: cluster_id,
|
||||
NAME: cluster.__class__.__name__
|
||||
})
|
||||
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg[ID],
|
||||
clusters
|
||||
))
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_ENTITY_CLUSTERS, websocket_entity_clusters,
|
||||
SCHEMA_WS_CLUSTERS
|
||||
)
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_ENTITY_CLUSTER_ATTRIBUTES, websocket_entity_cluster_attributes,
|
||||
SCHEMA_WS_CLUSTER_ATTRIBUTES
|
||||
)
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_ENTITY_CLUSTER_COMMANDS, websocket_entity_cluster_commands,
|
||||
SCHEMA_WS_CLUSTER_COMMANDS
|
||||
)
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_READ_CLUSTER_ATTRIBUTE, websocket_read_zigbee_cluster_attributes,
|
||||
SCHEMA_WS_READ_CLUSTER_ATTRIBUTE
|
||||
)
|
||||
|
||||
|
||||
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)
|
|
@ -36,6 +36,21 @@ DEFAULT_RADIO_TYPE = 'ezsp'
|
|||
DEFAULT_BAUDRATE = 57600
|
||||
DEFAULT_DATABASE_NAME = 'zigbee.db'
|
||||
|
||||
ATTR_CLUSTER_ID = 'cluster_id'
|
||||
ATTR_CLUSTER_TYPE = 'cluster_type'
|
||||
ATTR_ATTRIBUTE = 'attribute'
|
||||
ATTR_VALUE = 'value'
|
||||
ATTR_MANUFACTURER = 'manufacturer'
|
||||
ATTR_COMMAND = 'command'
|
||||
ATTR_COMMAND_TYPE = 'command_type'
|
||||
ATTR_ARGS = 'args'
|
||||
|
||||
IN = 'in'
|
||||
OUT = 'out'
|
||||
CLIENT_COMMANDS = 'client_commands'
|
||||
SERVER_COMMANDS = 'server_commands'
|
||||
SERVER = 'server'
|
||||
|
||||
|
||||
class RadioType(enum.Enum):
|
||||
"""Possible options for radio type."""
|
||||
|
|
|
@ -9,8 +9,11 @@ import logging
|
|||
from random import uniform
|
||||
|
||||
from homeassistant.components.zha.const import (
|
||||
DATA_ZHA, DATA_ZHA_BRIDGE_ID, DOMAIN)
|
||||
DATA_ZHA, DATA_ZHA_BRIDGE_ID, DOMAIN, ATTR_CLUSTER_ID, ATTR_ATTRIBUTE,
|
||||
ATTR_VALUE, ATTR_MANUFACTURER, ATTR_COMMAND, SERVER, ATTR_COMMAND_TYPE,
|
||||
ATTR_ARGS, IN, OUT, CLIENT_COMMANDS, SERVER_COMMANDS)
|
||||
from homeassistant.components.zha.helpers import bind_configure_reporting
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_FRIENDLY_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import entity
|
||||
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
|
||||
|
@ -18,6 +21,8 @@ from homeassistant.util import slugify
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ENTITY_SUFFIX = 'entity_suffix'
|
||||
|
||||
|
||||
class ZhaEntity(entity.Entity):
|
||||
"""A base class for ZHA entities."""
|
||||
|
@ -38,9 +43,9 @@ class ZhaEntity(entity.Entity):
|
|||
slugify(model),
|
||||
ieeetail,
|
||||
endpoint.endpoint_id,
|
||||
kwargs.get('entity_suffix', ''),
|
||||
kwargs.get(ENTITY_SUFFIX, ''),
|
||||
)
|
||||
self._device_state_attributes['friendly_name'] = "{} {}".format(
|
||||
self._device_state_attributes[CONF_FRIENDLY_NAME] = "{} {}".format(
|
||||
manufacturer,
|
||||
model,
|
||||
)
|
||||
|
@ -49,7 +54,7 @@ class ZhaEntity(entity.Entity):
|
|||
self._domain,
|
||||
ieeetail,
|
||||
endpoint.endpoint_id,
|
||||
kwargs.get('entity_suffix', ''),
|
||||
kwargs.get(ENTITY_SUFFIX, ''),
|
||||
)
|
||||
|
||||
self._endpoint = endpoint
|
||||
|
@ -69,6 +74,100 @@ class ZhaEntity(entity.Entity):
|
|||
self.manufacturer_code = None
|
||||
application_listener.register_entity(ieee, self)
|
||||
|
||||
async def get_clusters(self):
|
||||
"""Get zigbee clusters from this entity."""
|
||||
return {
|
||||
IN: self._in_clusters,
|
||||
OUT: self._out_clusters
|
||||
}
|
||||
|
||||
async def _get_cluster(self, cluster_id, cluster_type=IN):
|
||||
"""Get zigbee cluster from this entity."""
|
||||
if cluster_type == IN:
|
||||
cluster = self._in_clusters[cluster_id]
|
||||
else:
|
||||
cluster = self._out_clusters[cluster_id]
|
||||
if cluster is None:
|
||||
_LOGGER.warning('in_cluster with id: %s not found on entity: %s',
|
||||
cluster_id, self.entity_id)
|
||||
return cluster
|
||||
|
||||
async def get_cluster_attributes(self, cluster_id, cluster_type=IN):
|
||||
"""Get zigbee attributes for specified cluster."""
|
||||
cluster = await self._get_cluster(cluster_id, cluster_type)
|
||||
if cluster is None:
|
||||
return
|
||||
return cluster.attributes
|
||||
|
||||
async def write_zigbe_attribute(self, cluster_id, attribute, value,
|
||||
cluster_type=IN, manufacturer=None):
|
||||
"""Write a value to a zigbee attribute for a cluster in this entity."""
|
||||
cluster = await self._get_cluster(cluster_id, cluster_type)
|
||||
if cluster is None:
|
||||
return
|
||||
|
||||
from zigpy.exceptions import DeliveryError
|
||||
try:
|
||||
response = await cluster.write_attributes(
|
||||
{attribute: value},
|
||||
manufacturer=manufacturer
|
||||
)
|
||||
_LOGGER.debug(
|
||||
'set: %s for attr: %s to cluster: %s for entity: %s - res: %s',
|
||||
value,
|
||||
attribute,
|
||||
cluster_id,
|
||||
self.entity_id,
|
||||
response
|
||||
)
|
||||
return response
|
||||
except DeliveryError as exc:
|
||||
_LOGGER.debug(
|
||||
'failed to set attribute: %s %s %s %s %s',
|
||||
'{}: {}'.format(ATTR_VALUE, value),
|
||||
'{}: {}'.format(ATTR_ATTRIBUTE, attribute),
|
||||
'{}: {}'.format(ATTR_CLUSTER_ID, cluster_id),
|
||||
'{}: {}'.format(ATTR_ENTITY_ID, self.entity_id),
|
||||
exc
|
||||
)
|
||||
|
||||
async def get_cluster_commands(self, cluster_id, cluster_type=IN):
|
||||
"""Get zigbee commands for specified cluster."""
|
||||
cluster = await self._get_cluster(cluster_id, cluster_type)
|
||||
if cluster is None:
|
||||
return
|
||||
return {
|
||||
CLIENT_COMMANDS: cluster.client_commands,
|
||||
SERVER_COMMANDS: cluster.server_commands,
|
||||
}
|
||||
|
||||
async def issue_cluster_command(self, cluster_id, command, command_type,
|
||||
args, cluster_type=IN,
|
||||
manufacturer=None):
|
||||
"""Issue a command against specified zigbee cluster on this entity."""
|
||||
cluster = await self._get_cluster(cluster_id, cluster_type)
|
||||
if cluster is None:
|
||||
return
|
||||
response = None
|
||||
if command_type == SERVER:
|
||||
response = await cluster.command(command, *args,
|
||||
manufacturer=manufacturer,
|
||||
expect_reply=True)
|
||||
else:
|
||||
response = await cluster.client_command(command, *args)
|
||||
|
||||
_LOGGER.debug(
|
||||
'Issued cluster command: %s %s %s %s %s %s %s',
|
||||
'{}: {}'.format(ATTR_CLUSTER_ID, cluster_id),
|
||||
'{}: {}'.format(ATTR_COMMAND, command),
|
||||
'{}: {}'.format(ATTR_COMMAND_TYPE, command_type),
|
||||
'{}: {}'.format(ATTR_ARGS, args),
|
||||
'{}: {}'.format(ATTR_CLUSTER_ID, cluster_type),
|
||||
'{}: {}'.format(ATTR_MANUFACTURER, manufacturer),
|
||||
'{}: {}'.format(ATTR_ENTITY_ID, self.entity_id)
|
||||
)
|
||||
return response
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Handle entity addition to hass.
|
||||
|
||||
|
@ -201,9 +300,12 @@ class ZhaEntity(entity.Entity):
|
|||
return {
|
||||
'connections': {(CONNECTION_ZIGBEE, ieee)},
|
||||
'identifiers': {(DOMAIN, ieee)},
|
||||
'manufacturer': self._endpoint.manufacturer,
|
||||
ATTR_MANUFACTURER: self._endpoint.manufacturer,
|
||||
'model': self._endpoint.model,
|
||||
'name': self._device_state_attributes.get('friendly_name', ieee),
|
||||
'name': self._device_state_attributes.get(
|
||||
CONF_FRIENDLY_NAME,
|
||||
ieee
|
||||
),
|
||||
'via_hub': (DOMAIN, self.hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID]),
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,8 @@ from .const import (
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def safe_read(cluster, attributes, allow_cache=True, only_cache=False):
|
||||
async def safe_read(cluster, attributes, allow_cache=True, only_cache=False,
|
||||
manufacturer=None):
|
||||
"""Swallow all exceptions from network read.
|
||||
|
||||
If we throw during initialization, setup fails. Rather have an entity that
|
||||
|
@ -25,7 +26,8 @@ async def safe_read(cluster, attributes, allow_cache=True, only_cache=False):
|
|||
result, _ = await cluster.read_attributes(
|
||||
attributes,
|
||||
allow_cache=allow_cache,
|
||||
only_cache=only_cache
|
||||
only_cache=only_cache,
|
||||
manufacturer=manufacturer
|
||||
)
|
||||
return result
|
||||
except Exception: # pylint: disable=broad-except
|
||||
|
@ -124,3 +126,9 @@ async def check_zigpy_connection(usb_path, radio_type, database_path):
|
|||
except Exception: # pylint: disable=broad-except
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def convert_ieee(ieee_str):
|
||||
"""Convert given ieee string to EUI64."""
|
||||
from zigpy.types import EUI64, uint8_t
|
||||
return EUI64([uint8_t(p, base=16) for p in ieee_str.split(':')])
|
||||
|
|
|
@ -13,3 +13,63 @@ remove:
|
|||
ieee_address:
|
||||
description: IEEE address of the node to remove
|
||||
example: "00:0d:6f:00:05:7d:2d:34"
|
||||
|
||||
reconfigure_device:
|
||||
description: >-
|
||||
Reconfigure ZHA device (heal device). Use this if you are having issues
|
||||
with the device. If the device in question is a battery powered device
|
||||
please ensure it is awake and accepting commands when you use this
|
||||
service.
|
||||
fields:
|
||||
ieee_address:
|
||||
description: IEEE address of the device to reconfigure
|
||||
example: "00:0d:6f:00:05:7d:2d:34"
|
||||
|
||||
set_zigbee_cluster_attribute:
|
||||
description: >-
|
||||
Set attribute value for the specified cluster on the specified entity.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Entity id
|
||||
example: "binary_sensor.centralite_3130_00e8fb4e_1"
|
||||
cluster_id:
|
||||
description: ZCL cluster to retrieve attributes for
|
||||
example: 6
|
||||
cluster_type:
|
||||
description: type of the cluster (in or out)
|
||||
example: "out"
|
||||
attribute:
|
||||
description: id of the attribute to set
|
||||
example: 0
|
||||
value:
|
||||
description: value to write to the attribute
|
||||
example: 0x0001
|
||||
manufacturer:
|
||||
description: manufacturer code
|
||||
example: 0x00FC
|
||||
|
||||
issue_zigbee_cluster_command:
|
||||
description: >-
|
||||
Issue command on the specified cluster on the specified entity.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Entity id
|
||||
example: "binary_sensor.centralite_3130_00e8fb4e_1"
|
||||
cluster_id:
|
||||
description: ZCL cluster to retrieve attributes for
|
||||
example: 6
|
||||
cluster_type:
|
||||
description: type of the cluster (in or out)
|
||||
example: "out"
|
||||
command:
|
||||
description: id of the command to execute
|
||||
example: 0
|
||||
command_type:
|
||||
description: type of the command to execute (client or server)
|
||||
example: "server"
|
||||
args:
|
||||
description: args to pass to the command
|
||||
example: {}
|
||||
manufacturer:
|
||||
description: manufacturer code
|
||||
example: 0x00FC
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue