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:
David F. Mulcahey 2019-01-11 14:34:29 -05:00 committed by Paulus Schoutsen
parent a8f22287ca
commit 7be015fcc6
7 changed files with 637 additions and 45 deletions

View File

@ -426,6 +426,7 @@ omit =
homeassistant/components/zha/__init__.py homeassistant/components/zha/__init__.py
homeassistant/components/zha/const.py homeassistant/components/zha/const.py
homeassistant/components/zha/event.py homeassistant/components/zha/event.py
homeassistant/components/zha/api.py
homeassistant/components/zha/entities/* homeassistant/components/zha/entities/*
homeassistant/components/zha/helpers.py homeassistant/components/zha/helpers.py
homeassistant/components/*/zha.py homeassistant/components/*/zha.py

View File

@ -12,6 +12,7 @@ import types
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries, const as ha_const from homeassistant import config_entries, const as ha_const
from homeassistant.components.zha.entities import ZhaDeviceEntity from homeassistant.components.zha.entities import ZhaDeviceEntity
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE 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 config_flow # noqa # pylint: disable=unused-import
from . import const as zha_const from . import const as zha_const
from .event import ZhaEvent, ZhaRelayEvent from .event import ZhaEvent, ZhaRelayEvent
from . import api
from .helpers import convert_ieee
from .const import ( from .const import (
COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG, COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG,
CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, DATA_ZHA_BRIDGE_ID, CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, DATA_ZHA_BRIDGE_ID,
@ -56,22 +59,6 @@ CONFIG_SCHEMA = vol.Schema({
}) })
}, extra=vol.ALLOW_EXTRA) }, 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 # Zigbee definitions
CENTICELSIUS = 'C-100' CENTICELSIUS = 'C-100'
@ -179,25 +166,7 @@ async def async_setup_entry(hass, config_entry):
config_entry, component) config_entry, component)
) )
async def permit(service): api.async_load_api(hass, application_controller, listener)
"""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])
def zha_shutdown(event): def zha_shutdown(event):
"""Close radio.""" """Close radio."""
@ -209,8 +178,7 @@ async def async_setup_entry(hass, config_entry):
async def async_unload_entry(hass, config_entry): async def async_unload_entry(hass, config_entry):
"""Unload ZHA config entry.""" """Unload ZHA config entry."""
hass.services.async_remove(DOMAIN, SERVICE_PERMIT) api.async_unload_api(hass)
hass.services.async_remove(DOMAIN, SERVICE_REMOVE)
dispatchers = hass.data[DATA_ZHA].get(DATA_ZHA_DISPATCHERS, []) dispatchers = hass.data[DATA_ZHA].get(DATA_ZHA_DISPATCHERS, [])
for unsub_dispatcher in dispatchers: for unsub_dispatcher in dispatchers:
@ -285,6 +253,28 @@ class ApplicationListener:
if device.ieee in self._events: if device.ieee in self._events:
self._events.pop(device.ieee) 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): async def async_device_initialized(self, device, join):
"""Handle device joined and basic information discovered (async).""" """Handle device joined and basic information discovered (async)."""
import zigpy.profiles import zigpy.profiles

View 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)

View File

@ -36,6 +36,21 @@ DEFAULT_RADIO_TYPE = 'ezsp'
DEFAULT_BAUDRATE = 57600 DEFAULT_BAUDRATE = 57600
DEFAULT_DATABASE_NAME = 'zigbee.db' 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): class RadioType(enum.Enum):
"""Possible options for radio type.""" """Possible options for radio type."""

View File

@ -9,8 +9,11 @@ import logging
from random import uniform from random import uniform
from homeassistant.components.zha.const import ( 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.components.zha.helpers import bind_configure_reporting
from homeassistant.const import ATTR_ENTITY_ID, CONF_FRIENDLY_NAME
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import entity from homeassistant.helpers import entity
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
@ -18,6 +21,8 @@ from homeassistant.util import slugify
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ENTITY_SUFFIX = 'entity_suffix'
class ZhaEntity(entity.Entity): class ZhaEntity(entity.Entity):
"""A base class for ZHA entities.""" """A base class for ZHA entities."""
@ -38,9 +43,9 @@ class ZhaEntity(entity.Entity):
slugify(model), slugify(model),
ieeetail, ieeetail,
endpoint.endpoint_id, 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, manufacturer,
model, model,
) )
@ -49,7 +54,7 @@ class ZhaEntity(entity.Entity):
self._domain, self._domain,
ieeetail, ieeetail,
endpoint.endpoint_id, endpoint.endpoint_id,
kwargs.get('entity_suffix', ''), kwargs.get(ENTITY_SUFFIX, ''),
) )
self._endpoint = endpoint self._endpoint = endpoint
@ -69,6 +74,100 @@ class ZhaEntity(entity.Entity):
self.manufacturer_code = None self.manufacturer_code = None
application_listener.register_entity(ieee, self) 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): async def async_added_to_hass(self):
"""Handle entity addition to hass. """Handle entity addition to hass.
@ -201,9 +300,12 @@ class ZhaEntity(entity.Entity):
return { return {
'connections': {(CONNECTION_ZIGBEE, ieee)}, 'connections': {(CONNECTION_ZIGBEE, ieee)},
'identifiers': {(DOMAIN, ieee)}, 'identifiers': {(DOMAIN, ieee)},
'manufacturer': self._endpoint.manufacturer, ATTR_MANUFACTURER: self._endpoint.manufacturer,
'model': self._endpoint.model, '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]), 'via_hub': (DOMAIN, self.hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID]),
} }

View File

@ -14,7 +14,8 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _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. """Swallow all exceptions from network read.
If we throw during initialization, setup fails. Rather have an entity that 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( result, _ = await cluster.read_attributes(
attributes, attributes,
allow_cache=allow_cache, allow_cache=allow_cache,
only_cache=only_cache only_cache=only_cache,
manufacturer=manufacturer
) )
return result return result
except Exception: # pylint: disable=broad-except 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 except Exception: # pylint: disable=broad-except
return False return False
return True 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(':')])

View File

@ -13,3 +13,63 @@ remove:
ieee_address: ieee_address:
description: IEEE address of the node to remove description: IEEE address of the node to remove
example: "00:0d:6f:00:05:7d:2d:34" 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