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/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

View file

@ -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

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_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."""

View file

@ -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]),
}

View file

@ -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(':')])

View file

@ -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