ZHA component rewrite (#20434)

* rebase reorg

* update coveragerc for now

* sensor cleanup

* remove availability tracking for entities

* finish removing changes from tests

* review comments pass 1

* use asyncio.gather - review comments

* review comments

* cleanup - review comments

* review comments

* review comments

* cleanup

* cleanup - review comments

* review comments

* review comments

* use signal for removal

* correct comment

* remove entities from gateway

* remove dead module

* remove accidently committed file

* use named tuple - review comments

* squash bugs

* squash bugs

* add light and sensor back to coveragerc until % is higher
This commit is contained in:
David F. Mulcahey 2019-02-06 13:33:21 -05:00 committed by GitHub
parent 65a225da75
commit e6cd04d711
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1774 additions and 1591 deletions

View File

@ -658,7 +658,6 @@ omit =
homeassistant/components/zeroconf/* homeassistant/components/zeroconf/*
homeassistant/components/zha/__init__.py homeassistant/components/zha/__init__.py
homeassistant/components/zha/api.py homeassistant/components/zha/api.py
homeassistant/components/zha/binary_sensor.py
homeassistant/components/zha/const.py homeassistant/components/zha/const.py
homeassistant/components/zha/core/const.py homeassistant/components/zha/core/const.py
homeassistant/components/zha/core/device.py homeassistant/components/zha/core/device.py
@ -667,11 +666,8 @@ omit =
homeassistant/components/zha/core/listeners.py homeassistant/components/zha/core/listeners.py
homeassistant/components/zha/device_entity.py homeassistant/components/zha/device_entity.py
homeassistant/components/zha/entity.py homeassistant/components/zha/entity.py
homeassistant/components/zha/event.py
homeassistant/components/zha/fan.py
homeassistant/components/zha/light.py homeassistant/components/zha/light.py
homeassistant/components/zha/sensor.py homeassistant/components/zha/sensor.py
homeassistant/components/zha/switch.py
homeassistant/components/zigbee/* homeassistant/components/zigbee/*
homeassistant/components/zoneminder/* homeassistant/components/zoneminder/*
homeassistant/components/zwave/util.py homeassistant/components/zwave/util.py

View File

@ -4,6 +4,7 @@ Support for Zigbee Home Automation devices.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/ https://home-assistant.io/components/zha/
""" """
import asyncio
import logging import logging
import os import os
import types import types
@ -17,14 +18,15 @@ from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
# Loading the config flow file will register the flow # Loading the config flow file will register the flow
from . import config_flow # noqa # pylint: disable=unused-import from . import config_flow # noqa # pylint: disable=unused-import
from . import api from . import api
from .core.gateway import ZHAGateway from .core import ZHAGateway
from .const import ( from .core.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,
DATA_ZHA_CONFIG, DATA_ZHA_CORE_COMPONENT, DATA_ZHA_DISPATCHERS, DATA_ZHA_CONFIG, DATA_ZHA_CORE_COMPONENT, DATA_ZHA_DISPATCHERS,
DATA_ZHA_RADIO, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME, DATA_ZHA_RADIO, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME,
DEFAULT_RADIO_TYPE, DOMAIN, RadioType, DATA_ZHA_CORE_EVENTS, DEFAULT_RADIO_TYPE, DOMAIN, RadioType, DATA_ZHA_CORE_EVENTS, ENABLE_QUIRKS)
ENABLE_QUIRKS) from .core.gateway import establish_device_mappings
from .core.listeners import populate_listener_registry
REQUIREMENTS = [ REQUIREMENTS = [
'bellows==0.7.0', 'bellows==0.7.0',
@ -87,9 +89,16 @@ async def async_setup_entry(hass, config_entry):
Will automatically load components to support devices found on the network. Will automatically load components to support devices found on the network.
""" """
establish_device_mappings()
populate_listener_registry()
for component in COMPONENTS:
hass.data[DATA_ZHA][component] = (
hass.data[DATA_ZHA].get(component, {})
)
hass.data[DATA_ZHA] = hass.data.get(DATA_ZHA, {}) hass.data[DATA_ZHA] = hass.data.get(DATA_ZHA, {})
hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS] = [] hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS] = []
config = hass.data[DATA_ZHA].get(DATA_ZHA_CONFIG, {}) config = hass.data[DATA_ZHA].get(DATA_ZHA_CONFIG, {})
if config.get(ENABLE_QUIRKS, True): if config.get(ENABLE_QUIRKS, True):
@ -137,14 +146,32 @@ async def async_setup_entry(hass, config_entry):
ClusterPersistingListener ClusterPersistingListener
) )
application_controller = ControllerApplication(radio, database)
zha_gateway = ZHAGateway(hass, config) zha_gateway = ZHAGateway(hass, config)
hass.bus.async_listen_once(
ha_const.EVENT_HOMEASSISTANT_START, zha_gateway.accept_zigbee_messages)
# Patch handle_message until zigpy can provide an event here
def handle_message(sender, is_reply, profile, cluster,
src_ep, dst_ep, tsn, command_id, args):
"""Handle message from a device."""
if sender.last_seen is None and not sender.initializing:
if sender.ieee in zha_gateway.devices:
device = zha_gateway.devices[sender.ieee]
device.update_available(True)
return sender.handle_message(
is_reply, profile, cluster, src_ep, dst_ep, tsn, command_id, args)
application_controller = ControllerApplication(radio, database)
application_controller.handle_message = handle_message
application_controller.add_listener(zha_gateway) application_controller.add_listener(zha_gateway)
await application_controller.startup(auto_form=True) await application_controller.startup(auto_form=True)
hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(application_controller.ieee)
init_tasks = []
for device in application_controller.devices.values(): for device in application_controller.devices.values():
hass.async_create_task( init_tasks.append(zha_gateway.async_device_initialized(device, False))
zha_gateway.async_device_initialized(device, False)) await asyncio.gather(*init_tasks)
device_registry = await \ device_registry = await \
hass.helpers.device_registry.async_get_registry() hass.helpers.device_registry.async_get_registry()
@ -157,8 +184,6 @@ async def async_setup_entry(hass, config_entry):
model=radio_description, model=radio_description,
) )
hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(application_controller.ieee)
for component in COMPONENTS: for component in COMPONENTS:
hass.async_create_task( hass.async_create_task(
hass.config_entries.async_forward_entry_setup( hass.config_entries.async_forward_entry_setup(

View File

@ -11,8 +11,7 @@ import voluptuous as vol
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from .device_entity import ZhaDeviceEntity from .core.const import (
from .const import (
DOMAIN, ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, ATTR_ATTRIBUTE, ATTR_VALUE, DOMAIN, ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, ATTR_ATTRIBUTE, ATTR_VALUE,
ATTR_MANUFACTURER, ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ARGS, IN, OUT, ATTR_MANUFACTURER, ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ARGS, IN, OUT,
CLIENT_COMMANDS, SERVER_COMMANDS, SERVER) CLIENT_COMMANDS, SERVER_COMMANDS, SERVER)
@ -118,115 +117,7 @@ SCHEMA_WS_CLUSTER_COMMANDS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
}) })
@websocket_api.async_response def async_load_api(hass, application_controller, zha_gateway):
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.""" """Set up the web socket API."""
async def permit(service): async def permit(service):
"""Allow devices to join this network.""" """Allow devices to join this network."""
@ -256,11 +147,12 @@ def async_load_api(hass, application_controller, listener):
attribute = service.data.get(ATTR_ATTRIBUTE) attribute = service.data.get(ATTR_ATTRIBUTE)
value = service.data.get(ATTR_VALUE) value = service.data.get(ATTR_VALUE)
manufacturer = service.data.get(ATTR_MANUFACTURER) or None manufacturer = service.data.get(ATTR_MANUFACTURER) or None
component = hass.data.get(entity_id.split('.')[0]) entity_ref = zha_gateway.get_entity_reference(entity_id)
entity = component.get_entity(entity_id)
response = None response = None
if entity is not None: if entity_ref is not None:
response = await entity.write_zigbe_attribute( response = await entity_ref.zha_device.write_zigbee_attribute(
list(entity_ref.cluster_listeners.values())[
0].cluster.endpoint.endpoint_id,
cluster_id, cluster_id,
attribute, attribute,
value, value,
@ -292,11 +184,13 @@ def async_load_api(hass, application_controller, listener):
command_type = service.data.get(ATTR_COMMAND_TYPE) command_type = service.data.get(ATTR_COMMAND_TYPE)
args = service.data.get(ATTR_ARGS) args = service.data.get(ATTR_ARGS)
manufacturer = service.data.get(ATTR_MANUFACTURER) or None manufacturer = service.data.get(ATTR_MANUFACTURER) or None
component = hass.data.get(entity_id.split('.')[0]) entity_ref = zha_gateway.get_entity_reference(entity_id)
entity = component.get_entity(entity_id) zha_device = entity_ref.zha_device
response = None response = None
if entity is not None: if entity_ref is not None:
response = await entity.issue_cluster_command( response = await zha_device.issue_cluster_command(
list(entity_ref.cluster_listeners.values())[
0].cluster.endpoint.endpoint_id,
cluster_id, cluster_id,
command, command,
command_type, command_type,
@ -325,11 +219,9 @@ def async_load_api(hass, application_controller, listener):
async def websocket_reconfigure_node(hass, connection, msg): async def websocket_reconfigure_node(hass, connection, msg):
"""Reconfigure a ZHA nodes entities by its ieee address.""" """Reconfigure a ZHA nodes entities by its ieee address."""
ieee = msg[ATTR_IEEE] ieee = msg[ATTR_IEEE]
entities = listener.get_entities_for_ieee(ieee) device = zha_gateway.get_device(ieee)
_LOGGER.debug("Reconfiguring node with ieee_address: %s", ieee) _LOGGER.debug("Reconfiguring node with ieee_address: %s", ieee)
for entity in entities: hass.async_create_task(device.async_configure())
if hasattr(entity, 'async_configure'):
hass.async_create_task(entity.async_configure())
hass.components.websocket_api.async_register_command( hass.components.websocket_api.async_register_command(
WS_RECONFIGURE_NODE, websocket_reconfigure_node, WS_RECONFIGURE_NODE, websocket_reconfigure_node,
@ -340,15 +232,15 @@ def async_load_api(hass, application_controller, listener):
async def websocket_entities_by_ieee(hass, connection, msg): async def websocket_entities_by_ieee(hass, connection, msg):
"""Return a dict of all zha entities grouped by ieee.""" """Return a dict of all zha entities grouped by ieee."""
entities_by_ieee = {} entities_by_ieee = {}
for ieee, entities in listener.device_registry.items(): for ieee, entities in zha_gateway.device_registry.items():
ieee_string = str(ieee) ieee_string = str(ieee)
entities_by_ieee[ieee_string] = [] entities_by_ieee[ieee_string] = []
for entity in entities: for entity in entities:
if not isinstance(entity, ZhaDeviceEntity): entities_by_ieee[ieee_string].append({
entities_by_ieee[ieee_string].append({ ATTR_ENTITY_ID: entity.reference_id,
ATTR_ENTITY_ID: entity.entity_id, DEVICE_INFO: entity.device_info
DEVICE_INFO: entity.device_info })
})
connection.send_message(websocket_api.result_message( connection.send_message(websocket_api.result_message(
msg[ID], msg[ID],
entities_by_ieee entities_by_ieee
@ -363,24 +255,25 @@ def async_load_api(hass, application_controller, listener):
async def websocket_entity_clusters(hass, connection, msg): async def websocket_entity_clusters(hass, connection, msg):
"""Return a list of entity clusters.""" """Return a list of entity clusters."""
entity_id = msg[ATTR_ENTITY_ID] entity_id = msg[ATTR_ENTITY_ID]
entities = listener.get_entities_for_ieee(msg[ATTR_IEEE]) entity_ref = zha_gateway.get_entity_reference(entity_id)
entity = next(
ent for ent in entities if ent.entity_id == entity_id)
entity_clusters = await entity.get_clusters()
clusters = [] clusters = []
if entity_ref is not None:
for cluster_id, cluster in entity_clusters[IN].items(): for listener in entity_ref.cluster_listeners.values():
clusters.append({ cluster = listener.cluster
TYPE: IN, in_clusters = cluster.endpoint.in_clusters.values()
ID: cluster_id, out_clusters = cluster.endpoint.out_clusters.values()
NAME: cluster.__class__.__name__ if cluster in in_clusters:
}) clusters.append({
for cluster_id, cluster in entity_clusters[OUT].items(): TYPE: IN,
clusters.append({ ID: cluster.cluster_id,
TYPE: OUT, NAME: cluster.__class__.__name__
ID: cluster_id, })
NAME: cluster.__class__.__name__ elif cluster in out_clusters:
}) clusters.append({
TYPE: OUT,
ID: cluster.cluster_id,
NAME: cluster.__class__.__name__
})
connection.send_message(websocket_api.result_message( connection.send_message(websocket_api.result_message(
msg[ID], msg[ID],
@ -392,16 +285,141 @@ def async_load_api(hass, application_controller, listener):
SCHEMA_WS_CLUSTERS SCHEMA_WS_CLUSTERS
) )
@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]
ieee = msg[ATTR_IEEE]
cluster_attributes = []
entity_ref = zha_gateway.get_entity_reference(entity_id)
device = zha_gateway.get_device(ieee)
attributes = None
if entity_ref is not None:
attributes = await device.get_cluster_attributes(
list(entity_ref.cluster_listeners.values())[
0].cluster.endpoint.endpoint_id,
cluster_id,
cluster_type)
if attributes is not None:
for attr_id in attributes:
cluster_attributes.append(
{
ID: attr_id,
NAME: attributes[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
))
hass.components.websocket_api.async_register_command( hass.components.websocket_api.async_register_command(
WS_ENTITY_CLUSTER_ATTRIBUTES, websocket_entity_cluster_attributes, WS_ENTITY_CLUSTER_ATTRIBUTES, websocket_entity_cluster_attributes,
SCHEMA_WS_CLUSTER_ATTRIBUTES SCHEMA_WS_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]
ieee = msg[ATTR_IEEE]
entity_ref = zha_gateway.get_entity_reference(entity_id)
device = zha_gateway.get_device(ieee)
cluster_commands = []
commands = None
if entity_ref is not None:
commands = await device.get_cluster_commands(
list(entity_ref.cluster_listeners.values())[
0].cluster.endpoint.endpoint_id,
cluster_id,
cluster_type)
if commands is not None:
for cmd_id in commands[CLIENT_COMMANDS]:
cluster_commands.append(
{
TYPE: CLIENT,
ID: cmd_id,
NAME: commands[CLIENT_COMMANDS][cmd_id][0]
}
)
for cmd_id in commands[SERVER_COMMANDS]:
cluster_commands.append(
{
TYPE: SERVER,
ID: cmd_id,
NAME: commands[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
))
hass.components.websocket_api.async_register_command( hass.components.websocket_api.async_register_command(
WS_ENTITY_CLUSTER_COMMANDS, websocket_entity_cluster_commands, WS_ENTITY_CLUSTER_COMMANDS, websocket_entity_cluster_commands,
SCHEMA_WS_CLUSTER_COMMANDS SCHEMA_WS_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]
entity_ref = zha_gateway.get_entity_reference(entity_id)
manufacturer = msg.get(ATTR_MANUFACTURER) or None
success = failure = None
clusters = []
if cluster_type == IN:
clusters = \
list(entity_ref.cluster_listeners.values())[
0].cluster.endpoint.in_clusters
else:
clusters = \
list(entity_ref.cluster_listeners.values())[
0].cluster.endpoint.out_clusters
cluster = clusters[cluster_id]
if entity_ref 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))
))
hass.components.websocket_api.async_register_command( hass.components.websocket_api.async_register_command(
WS_READ_CLUSTER_ATTRIBUTE, websocket_read_zigbee_cluster_attributes, WS_READ_CLUSTER_ATTRIBUTE, websocket_read_zigbee_cluster_attributes,
SCHEMA_WS_READ_CLUSTER_ATTRIBUTE SCHEMA_WS_READ_CLUSTER_ATTRIBUTE

View File

@ -7,16 +7,13 @@ at https://home-assistant.io/components/binary_sensor.zha/
import logging import logging
from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice
from homeassistant.const import STATE_ON
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.restore_state import RestoreEntity
from .core import helpers
from .core.const import ( from .core.const import (
DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW) DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, LISTENER_ON_OFF,
LISTENER_LEVEL, LISTENER_ZONE, SIGNAL_ATTR_UPDATED, SIGNAL_MOVE_LEVEL,
SIGNAL_SET_LEVEL, LISTENER_ATTRIBUTE, UNKNOWN, OPENING, ZONE, OCCUPANCY,
ATTR_LEVEL, SENSOR_TYPE)
from .entity import ZhaEntity from .entity import ZhaEntity
from .core.listeners import (
OnOffListener, LevelListener
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -31,7 +28,20 @@ CLASS_MAPPING = {
0x002b: 'gas', 0x002b: 'gas',
0x002d: 'vibration', 0x002d: 'vibration',
} }
DEVICE_CLASS_OCCUPANCY = 'occupancy'
async def get_ias_device_class(listener):
"""Get the HA device class from the listener."""
zone_type = await listener.get_attribute_value('zone_type')
return CLASS_MAPPING.get(zone_type)
DEVICE_CLASS_REGISTRY = {
UNKNOWN: None,
OPENING: OPENING,
ZONE: get_ias_device_class,
OCCUPANCY: OCCUPANCY,
}
async def async_setup_platform(hass, config, async_add_entities, async def async_setup_platform(hass, config, async_add_entities,
@ -60,249 +70,60 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def _async_setup_entities(hass, config_entry, async_add_entities, async def _async_setup_entities(hass, config_entry, async_add_entities,
discovery_infos): discovery_infos):
"""Set up the ZHA binary sensors.""" """Set up the ZHA binary sensors."""
from zigpy.zcl.clusters.general import OnOff
from zigpy.zcl.clusters.measurement import OccupancySensing
from zigpy.zcl.clusters.security import IasZone
entities = [] entities = []
for discovery_info in discovery_infos: for discovery_info in discovery_infos:
if IasZone.cluster_id in discovery_info['in_clusters']: entities.append(BinarySensor(**discovery_info))
entities.append(await _async_setup_iaszone(discovery_info))
elif OccupancySensing.cluster_id in discovery_info['in_clusters']:
entities.append(
BinarySensor(DEVICE_CLASS_OCCUPANCY, **discovery_info))
elif OnOff.cluster_id in discovery_info['out_clusters']:
entities.append(Remote(**discovery_info))
async_add_entities(entities, update_before_add=True) async_add_entities(entities, update_before_add=True)
async def _async_setup_iaszone(discovery_info): class BinarySensor(ZhaEntity, BinarySensorDevice):
device_class = None """ZHA BinarySensor."""
from zigpy.zcl.clusters.security import IasZone
cluster = discovery_info['in_clusters'][IasZone.cluster_id]
try:
zone_type = await cluster['zone_type']
device_class = CLASS_MAPPING.get(zone_type, None)
except Exception: # pylint: disable=broad-except
# If we fail to read from the device, use a non-specific class
pass
return IasZoneSensor(device_class, **discovery_info)
class IasZoneSensor(RestoreEntity, ZhaEntity, BinarySensorDevice):
"""The IasZoneSensor Binary Sensor."""
_domain = DOMAIN
def __init__(self, device_class, **kwargs):
"""Initialize the ZHA binary sensor."""
super().__init__(**kwargs)
self._device_class = device_class
from zigpy.zcl.clusters.security import IasZone
self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id]
@property
def is_on(self) -> bool:
"""Return True if entity is on."""
if self._state is None:
return False
return bool(self._state)
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return self._device_class
def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster."""
if command_id == 0:
self._state = args[0] & 3
_LOGGER.debug("Updated alarm state: %s", self._state)
self.async_schedule_update_ha_state()
elif command_id == 1:
_LOGGER.debug("Enroll requested")
res = self._ias_zone_cluster.enroll_response(0, 0)
self.hass.async_add_job(res)
async def async_added_to_hass(self):
"""Run when about to be added to hass."""
await super().async_added_to_hass()
old_state = await self.async_get_last_state()
if self._state is not None or old_state is None:
return
_LOGGER.debug("%s restoring old state: %s", self.entity_id, old_state)
if old_state.state == STATE_ON:
self._state = 3
else:
self._state = 0
async def async_configure(self):
"""Configure IAS device."""
await self._ias_zone_cluster.bind()
ieee = self._ias_zone_cluster.endpoint.device.application.ieee
await self._ias_zone_cluster.write_attributes({'cie_addr': ieee})
_LOGGER.debug("%s: finished configuration", self.entity_id)
async def async_update(self):
"""Retrieve latest state."""
from zigpy.types.basic import uint16_t
result = await helpers.safe_read(self._endpoint.ias_zone,
['zone_status'],
allow_cache=False,
only_cache=(not self._initialized))
state = result.get('zone_status', self._state)
if isinstance(state, (int, uint16_t)):
self._state = result.get('zone_status', self._state) & 3
class Remote(RestoreEntity, ZhaEntity, BinarySensorDevice):
"""ZHA switch/remote controller/button."""
_domain = DOMAIN
def __init__(self, **kwargs):
"""Initialize Switch."""
super().__init__(**kwargs)
self._level = 0
from zigpy.zcl.clusters import general
self._out_listeners = {
general.OnOff.cluster_id: OnOffListener(
self,
self._out_clusters[general.OnOff.cluster_id]
)
}
out_clusters = kwargs.get('out_clusters')
self._zcl_reporting = {}
if general.LevelControl.cluster_id in out_clusters:
self._out_listeners.update({
general.LevelControl.cluster_id: LevelListener(
self,
out_clusters[general.LevelControl.cluster_id]
)
})
@property
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
return self._state
@property
def device_state_attributes(self):
"""Return the device state attributes."""
self._device_state_attributes.update({
'level': self._state and self._level or 0
})
return self._device_state_attributes
@property
def zcl_reporting_config(self):
"""Return ZCL attribute reporting configuration."""
return self._zcl_reporting
def move_level(self, change):
"""Increment the level, setting state if appropriate."""
if not self._state and change > 0:
self._level = 0
self._level = min(255, max(0, self._level + change))
self._state = bool(self._level)
self.async_schedule_update_ha_state()
def set_level(self, level):
"""Set the level, setting state if appropriate."""
self._level = level
self._state = bool(self._level)
self.async_schedule_update_ha_state()
def set_state(self, state):
"""Set the state."""
self._state = state
if self._level == 0:
self._level = 255
self.async_schedule_update_ha_state()
async def async_configure(self):
"""Bind clusters."""
from zigpy.zcl.clusters import general
await helpers.bind_cluster(
self.entity_id,
self._out_clusters[general.OnOff.cluster_id]
)
if general.LevelControl.cluster_id in self._out_clusters:
await helpers.bind_cluster(
self.entity_id,
self._out_clusters[general.LevelControl.cluster_id]
)
async def async_added_to_hass(self):
"""Run when about to be added to hass."""
await super().async_added_to_hass()
old_state = await self.async_get_last_state()
if self._state is not None or old_state is None:
return
_LOGGER.debug("%s restoring old state: %s", self.entity_id, old_state)
if 'level' in old_state.attributes:
self._level = old_state.attributes['level']
self._state = old_state.state == STATE_ON
async def async_update(self):
"""Retrieve latest state."""
from zigpy.zcl.clusters.general import OnOff
result = await helpers.safe_read(
self._endpoint.out_clusters[OnOff.cluster_id],
['on_off'],
allow_cache=False,
only_cache=(not self._initialized)
)
self._state = result.get('on_off', self._state)
class BinarySensor(RestoreEntity, ZhaEntity, BinarySensorDevice):
"""ZHA switch."""
_domain = DOMAIN _domain = DOMAIN
_device_class = None _device_class = None
value_attribute = 0
def __init__(self, device_class, **kwargs): def __init__(self, **kwargs):
"""Initialize the ZHA binary sensor.""" """Initialize the ZHA binary sensor."""
super().__init__(**kwargs) super().__init__(**kwargs)
self._device_class = device_class self._device_state_attributes = {}
self._cluster = list(kwargs['in_clusters'].values())[0] self._zone_listener = self.cluster_listeners.get(LISTENER_ZONE)
self._on_off_listener = self.cluster_listeners.get(LISTENER_ON_OFF)
self._level_listener = self.cluster_listeners.get(LISTENER_LEVEL)
self._attr_listener = self.cluster_listeners.get(LISTENER_ATTRIBUTE)
self._zha_sensor_type = kwargs[SENSOR_TYPE]
self._level = None
def attribute_updated(self, attribute, value): async def _determine_device_class(self):
"""Handle attribute update from device.""" """Determine the device class for this binary sensor."""
_LOGGER.debug("Attribute updated: %s %s %s", self, attribute, value) device_class_supplier = DEVICE_CLASS_REGISTRY.get(
if attribute == self.value_attribute: self._zha_sensor_type)
self._state = bool(value) if callable(device_class_supplier):
self.async_schedule_update_ha_state() listener = self.cluster_listeners.get(self._zha_sensor_type)
if listener is None:
return None
return await device_class_supplier(listener)
return device_class_supplier
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Run when about to be added to hass.""" """Run when about to be added to hass."""
self._device_class = await self._determine_device_class()
await super().async_added_to_hass() await super().async_added_to_hass()
old_state = await self.async_get_last_state() if self._level_listener:
if self._state is not None or old_state is None: await self.async_accept_signal(
return self._level_listener, SIGNAL_SET_LEVEL, self.set_level)
await self.async_accept_signal(
_LOGGER.debug("%s restoring old state: %s", self.entity_id, old_state) self._level_listener, SIGNAL_MOVE_LEVEL, self.move_level)
self._state = old_state.state == STATE_ON if self._on_off_listener:
await self.async_accept_signal(
@property self._on_off_listener, SIGNAL_ATTR_UPDATED,
def cluster(self): self.async_set_state)
"""Zigbee cluster for this entity.""" if self._zone_listener:
return self._cluster await self.async_accept_signal(
self._zone_listener, SIGNAL_ATTR_UPDATED, self.async_set_state)
@property if self._attr_listener:
def zcl_reporting_config(self): await self.async_accept_signal(
"""ZHA reporting configuration.""" self._attr_listener, SIGNAL_ATTR_UPDATED, self.async_set_state)
return {self.cluster: {self.value_attribute: REPORT_CONFIG_IMMEDIATE}}
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
@ -315,3 +136,32 @@ class BinarySensor(RestoreEntity, ZhaEntity, BinarySensorDevice):
def device_class(self) -> str: def device_class(self) -> str:
"""Return device class from component DEVICE_CLASSES.""" """Return device class from component DEVICE_CLASSES."""
return self._device_class return self._device_class
def async_set_state(self, state):
"""Set the state."""
self._state = bool(state)
self.async_schedule_update_ha_state()
def move_level(self, change):
"""Increment the level, setting state if appropriate."""
level = self._level or 0
if not self._state and change > 0:
level = 0
self._level = min(254, max(0, level + change))
self._state = bool(self._level)
self.async_schedule_update_ha_state()
def set_level(self, level):
"""Set the level, setting state if appropriate."""
self._level = level
self._state = bool(level)
self.async_schedule_update_ha_state()
@property
def device_state_attributes(self):
"""Return the device state attributes."""
if self._level_listener is not None:
self._device_state_attributes.update({
ATTR_LEVEL: self._state and self._level or 0
})
return self._device_state_attributes

View File

@ -4,3 +4,10 @@ Core module for Zigbee Home Automation.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/ https://home-assistant.io/components/zha/
""" """
# flake8: noqa
from .device import ZHADevice
from .gateway import ZHAGateway
from .listeners import (
ClusterListener, AttributeListener, OnOffListener, LevelListener,
IASZoneListener, ActivePowerListener, BatteryListener, EventRelayListener)

View File

@ -55,10 +55,38 @@ IEEE = 'ieee'
MODEL = 'model' MODEL = 'model'
NAME = 'name' NAME = 'name'
SENSOR_TYPE = 'sensor_type'
HUMIDITY = 'humidity'
TEMPERATURE = 'temperature'
ILLUMINANCE = 'illuminance'
PRESSURE = 'pressure'
METERING = 'metering'
ELECTRICAL_MEASUREMENT = 'electrical_measurement'
POWER_CONFIGURATION = 'power_configuration'
GENERIC = 'generic'
UNKNOWN = 'unknown'
OPENING = 'opening'
ZONE = 'zone'
OCCUPANCY = 'occupancy'
ATTR_LEVEL = 'level'
LISTENER_ON_OFF = 'on_off'
LISTENER_ATTRIBUTE = 'attribute'
LISTENER_COLOR = 'color'
LISTENER_FAN = 'fan'
LISTENER_LEVEL = ATTR_LEVEL
LISTENER_ZONE = 'zone'
LISTENER_ACTIVE_POWER = 'active_power'
LISTENER_BATTERY = 'battery' LISTENER_BATTERY = 'battery'
LISTENER_EVENT_RELAY = 'event_relay'
SIGNAL_ATTR_UPDATED = 'attribute_updated' SIGNAL_ATTR_UPDATED = 'attribute_updated'
SIGNAL_MOVE_LEVEL = "move_level"
SIGNAL_SET_LEVEL = "set_level"
SIGNAL_STATE_ATTR = "update_state_attribute"
SIGNAL_AVAILABLE = 'available' SIGNAL_AVAILABLE = 'available'
SIGNAL_REMOVE = 'remove'
class RadioType(enum.Enum): class RadioType(enum.Enum):
@ -78,9 +106,10 @@ DISCOVERY_KEY = 'zha_discovery_info'
DEVICE_CLASS = {} DEVICE_CLASS = {}
SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {} SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {}
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {} SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {}
CLUSTER_REPORT_CONFIGS = {}
CUSTOM_CLUSTER_MAPPINGS = {} CUSTOM_CLUSTER_MAPPINGS = {}
COMPONENT_CLUSTERS = {} COMPONENT_CLUSTERS = {}
EVENTABLE_CLUSTERS = [] EVENT_RELAY_CLUSTERS = []
REPORT_CONFIG_MAX_INT = 900 REPORT_CONFIG_MAX_INT = 900
REPORT_CONFIG_MAX_INT_BATTERY_SAVE = 10800 REPORT_CONFIG_MAX_INT_BATTERY_SAVE = 10800

View File

@ -14,7 +14,7 @@ from .const import (
ATTR_MANUFACTURER, LISTENER_BATTERY, SIGNAL_AVAILABLE, IN, OUT, ATTR_MANUFACTURER, LISTENER_BATTERY, SIGNAL_AVAILABLE, IN, OUT,
ATTR_CLUSTER_ID, ATTR_ATTRIBUTE, ATTR_VALUE, ATTR_COMMAND, SERVER, ATTR_CLUSTER_ID, ATTR_ATTRIBUTE, ATTR_VALUE, ATTR_COMMAND, SERVER,
ATTR_COMMAND_TYPE, ATTR_ARGS, CLIENT_COMMANDS, SERVER_COMMANDS, ATTR_COMMAND_TYPE, ATTR_ARGS, CLIENT_COMMANDS, SERVER_COMMANDS,
ATTR_ENDPOINT_ID, IEEE, MODEL, NAME ATTR_ENDPOINT_ID, IEEE, MODEL, NAME, UNKNOWN
) )
from .listeners import EventRelayListener from .listeners import EventRelayListener
@ -30,11 +30,14 @@ class ZHADevice:
self._zigpy_device = zigpy_device self._zigpy_device = zigpy_device
# Get first non ZDO endpoint id to use to get manufacturer and model # Get first non ZDO endpoint id to use to get manufacturer and model
endpoint_ids = zigpy_device.endpoints.keys() endpoint_ids = zigpy_device.endpoints.keys()
ept_id = next(ept_id for ept_id in endpoint_ids if ept_id != 0) self._manufacturer = UNKNOWN
self._manufacturer = zigpy_device.endpoints[ept_id].manufacturer self._model = UNKNOWN
self._model = zigpy_device.endpoints[ept_id].model ept_id = next((ept_id for ept_id in endpoint_ids if ept_id != 0), None)
if ept_id is not None:
self._manufacturer = zigpy_device.endpoints[ept_id].manufacturer
self._model = zigpy_device.endpoints[ept_id].model
self._zha_gateway = zha_gateway self._zha_gateway = zha_gateway
self._cluster_listeners = {} self.cluster_listeners = {}
self._relay_listeners = [] self._relay_listeners = []
self._all_listeners = [] self._all_listeners = []
self._name = "{} {}".format( self._name = "{} {}".format(
@ -101,21 +104,11 @@ class ZHADevice:
"""Return the gateway for this device.""" """Return the gateway for this device."""
return self._zha_gateway return self._zha_gateway
@property
def cluster_listeners(self):
"""Return cluster listeners for device."""
return self._cluster_listeners.values()
@property @property
def all_listeners(self): def all_listeners(self):
"""Return cluster listeners and relay listeners for device.""" """Return cluster listeners and relay listeners for device."""
return self._all_listeners return self._all_listeners
@property
def cluster_listener_keys(self):
"""Return cluster listeners for device."""
return self._cluster_listeners.keys()
@property @property
def available_signal(self): def available_signal(self):
"""Signal to use to subscribe to device availability changes.""" """Signal to use to subscribe to device availability changes."""
@ -157,17 +150,13 @@ class ZHADevice:
"""Add cluster listener to device.""" """Add cluster listener to device."""
# only keep 1 power listener # only keep 1 power listener
if cluster_listener.name is LISTENER_BATTERY and \ if cluster_listener.name is LISTENER_BATTERY and \
LISTENER_BATTERY in self._cluster_listeners: LISTENER_BATTERY in self.cluster_listeners:
return return
self._all_listeners.append(cluster_listener) self._all_listeners.append(cluster_listener)
if isinstance(cluster_listener, EventRelayListener): if isinstance(cluster_listener, EventRelayListener):
self._relay_listeners.append(cluster_listener) self._relay_listeners.append(cluster_listener)
else: else:
self._cluster_listeners[cluster_listener.name] = cluster_listener self.cluster_listeners[cluster_listener.name] = cluster_listener
def get_cluster_listener(self, name):
"""Get cluster listener by name."""
return self._cluster_listeners.get(name, None)
async def async_configure(self): async def async_configure(self):
"""Configure the device.""" """Configure the device."""

View File

@ -5,7 +5,9 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/ https://home-assistant.io/components/zha/
""" """
import asyncio
import collections import collections
import itertools
import logging import logging
from homeassistant import const as ha_const from homeassistant import const as ha_const
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
@ -13,15 +15,27 @@ from homeassistant.helpers.entity_component import EntityComponent
from . import const as zha_const from . import const as zha_const
from .const import ( from .const import (
COMPONENTS, CONF_DEVICE_CONFIG, DATA_ZHA, DATA_ZHA_CORE_COMPONENT, DOMAIN, COMPONENTS, CONF_DEVICE_CONFIG, DATA_ZHA, DATA_ZHA_CORE_COMPONENT, DOMAIN,
ZHA_DISCOVERY_NEW, EVENTABLE_CLUSTERS, DATA_ZHA_CORE_EVENTS, DEVICE_CLASS, ZHA_DISCOVERY_NEW, DEVICE_CLASS, SINGLE_INPUT_CLUSTER_DEVICE_CLASS,
SINGLE_INPUT_CLUSTER_DEVICE_CLASS, SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, COMPONENT_CLUSTERS, HUMIDITY,
CUSTOM_CLUSTER_MAPPINGS, COMPONENT_CLUSTERS) TEMPERATURE, ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT,
POWER_CONFIGURATION, GENERIC, SENSOR_TYPE, EVENT_RELAY_CLUSTERS,
LISTENER_BATTERY, UNKNOWN, OPENING, ZONE, OCCUPANCY,
CLUSTER_REPORT_CONFIGS, REPORT_CONFIG_IMMEDIATE, REPORT_CONFIG_ASAP,
REPORT_CONFIG_DEFAULT, REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT,
REPORT_CONFIG_OP, SIGNAL_REMOVE)
from .device import ZHADevice
from ..device_entity import ZhaDeviceEntity from ..device_entity import ZhaDeviceEntity
from ..event import ZhaEvent, ZhaRelayEvent from .listeners import (
LISTENER_REGISTRY, AttributeListener, EventRelayListener, ZDOListener)
from .helpers import convert_ieee from .helpers import convert_ieee
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {}
BINARY_SENSOR_TYPES = {}
EntityReference = collections.namedtuple(
'EntityReference', 'reference_id zha_device cluster_listeners device_info')
class ZHAGateway: class ZHAGateway:
"""Gateway that handles events that happen on the ZHA Zigbee network.""" """Gateway that handles events that happen on the ZHA Zigbee network."""
@ -31,16 +45,9 @@ class ZHAGateway:
self._hass = hass self._hass = hass
self._config = config self._config = config
self._component = EntityComponent(_LOGGER, DOMAIN, hass) self._component = EntityComponent(_LOGGER, DOMAIN, hass)
self._devices = {}
self._device_registry = collections.defaultdict(list) self._device_registry = collections.defaultdict(list)
self._events = {}
establish_device_mappings()
for component in COMPONENTS:
hass.data[DATA_ZHA][component] = (
hass.data[DATA_ZHA].get(component, {})
)
hass.data[DATA_ZHA][DATA_ZHA_CORE_COMPONENT] = self._component hass.data[DATA_ZHA][DATA_ZHA_CORE_COMPONENT] = self._component
hass.data[DATA_ZHA][DATA_ZHA_CORE_EVENTS] = self._events
def device_joined(self, device): def device_joined(self, device):
"""Handle device joined. """Handle device joined.
@ -67,197 +74,310 @@ class ZHAGateway:
def device_removed(self, device): def device_removed(self, device):
"""Handle device being removed from the network.""" """Handle device being removed from the network."""
for device_entity in self._device_registry[device.ieee]: device = self._devices.pop(device.ieee, None)
self._hass.async_create_task(device_entity.async_remove()) self._device_registry.pop(device.ieee, None)
if device.ieee in self._events: if device is not None:
self._events.pop(device.ieee) self._hass.async_create_task(device.async_unsub_dispatcher())
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
device_manufacturer = device_model = None
for endpoint_id, endpoint in device.endpoints.items():
if endpoint_id == 0: # ZDO
continue
if endpoint.manufacturer is not None:
device_manufacturer = endpoint.manufacturer
if endpoint.model is not None:
device_model = endpoint.model
component = None
profile_clusters = ([], [])
device_key = "{}-{}".format(device.ieee, endpoint_id)
node_config = {}
if CONF_DEVICE_CONFIG in self._config:
node_config = self._config[CONF_DEVICE_CONFIG].get(
device_key, {}
)
if endpoint.profile_id in zigpy.profiles.PROFILES:
profile = zigpy.profiles.PROFILES[endpoint.profile_id]
if zha_const.DEVICE_CLASS.get(endpoint.profile_id,
{}).get(endpoint.device_type,
None):
profile_clusters = profile.CLUSTERS[endpoint.device_type]
profile_info = zha_const.DEVICE_CLASS[endpoint.profile_id]
component = profile_info[endpoint.device_type]
if ha_const.CONF_TYPE in node_config:
component = node_config[ha_const.CONF_TYPE]
profile_clusters = zha_const.COMPONENT_CLUSTERS[component]
if component:
in_clusters = [endpoint.in_clusters[c]
for c in profile_clusters[0]
if c in endpoint.in_clusters]
out_clusters = [endpoint.out_clusters[c]
for c in profile_clusters[1]
if c in endpoint.out_clusters]
discovery_info = {
'application_listener': self,
'endpoint': endpoint,
'in_clusters': {c.cluster_id: c for c in in_clusters},
'out_clusters': {c.cluster_id: c for c in out_clusters},
'manufacturer': endpoint.manufacturer,
'model': endpoint.model,
'new_join': join,
'unique_id': device_key,
}
if join:
async_dispatcher_send(
self._hass,
ZHA_DISCOVERY_NEW.format(component),
discovery_info
)
else:
self._hass.data[DATA_ZHA][component][device_key] = (
discovery_info
)
for cluster in endpoint.in_clusters.values():
await self._attempt_single_cluster_device(
endpoint,
cluster,
profile_clusters[0],
device_key,
zha_const.SINGLE_INPUT_CLUSTER_DEVICE_CLASS,
'in_clusters',
join,
)
for cluster in endpoint.out_clusters.values():
await self._attempt_single_cluster_device(
endpoint,
cluster,
profile_clusters[1],
device_key,
zha_const.SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS,
'out_clusters',
join,
)
endpoint_entity = ZhaDeviceEntity(
device,
device_manufacturer,
device_model,
self,
)
await self._component.async_add_entities([endpoint_entity])
def register_entity(self, ieee, entity_obj):
"""Record the creation of a hass entity associated with ieee."""
self._device_registry[ieee].append(entity_obj)
async def _attempt_single_cluster_device(self, endpoint, cluster,
profile_clusters, device_key,
device_classes, discovery_attr,
is_new_join):
"""Try to set up an entity from a "bare" cluster."""
if cluster.cluster_id in EVENTABLE_CLUSTERS:
if cluster.endpoint.device.ieee not in self._events:
self._events.update({cluster.endpoint.device.ieee: []})
from zigpy.zcl.clusters.general import OnOff, LevelControl
if discovery_attr == 'out_clusters' and \
(cluster.cluster_id == OnOff.cluster_id or
cluster.cluster_id == LevelControl.cluster_id):
self._events[cluster.endpoint.device.ieee].append(
ZhaRelayEvent(self._hass, cluster)
)
else:
self._events[cluster.endpoint.device.ieee].append(ZhaEvent(
self._hass,
cluster
))
if cluster.cluster_id in profile_clusters:
return
component = sub_component = None
for cluster_type, candidate_component in device_classes.items():
if isinstance(cluster, cluster_type):
component = candidate_component
break
for signature, comp in zha_const.CUSTOM_CLUSTER_MAPPINGS.items():
if (isinstance(endpoint.device, signature[0]) and
cluster.cluster_id == signature[1]):
component = comp[0]
sub_component = comp[1]
break
if component is None:
return
cluster_key = "{}-{}".format(device_key, cluster.cluster_id)
discovery_info = {
'application_listener': self,
'endpoint': endpoint,
'in_clusters': {},
'out_clusters': {},
'manufacturer': endpoint.manufacturer,
'model': endpoint.model,
'new_join': is_new_join,
'unique_id': cluster_key,
'entity_suffix': '_{}'.format(cluster.cluster_id),
}
discovery_info[discovery_attr] = {cluster.cluster_id: cluster}
if sub_component:
discovery_info.update({'sub_component': sub_component})
if is_new_join:
async_dispatcher_send( async_dispatcher_send(
self._hass, self._hass,
ZHA_DISCOVERY_NEW.format(component), "{}_{}".format(SIGNAL_REMOVE, str(device.ieee))
discovery_info
) )
else:
self._hass.data[DATA_ZHA][component][cluster_key] = discovery_info def get_device(self, ieee_str):
"""Return ZHADevice for given ieee."""
ieee = convert_ieee(ieee_str)
return self._devices.get(ieee)
def get_entity_reference(self, entity_id):
"""Return entity reference for given entity_id if found."""
for entity_reference in itertools.chain.from_iterable(
self.device_registry.values()):
if entity_id == entity_reference.reference_id:
return entity_reference
@property
def devices(self):
"""Return devices."""
return self._devices
@property
def device_registry(self):
"""Return entities by ieee."""
return self._device_registry
def register_entity_reference(
self, ieee, reference_id, zha_device, cluster_listeners,
device_info):
"""Record the creation of a hass entity associated with ieee."""
self._device_registry[ieee].append(
EntityReference(
reference_id=reference_id,
zha_device=zha_device,
cluster_listeners=cluster_listeners,
device_info=device_info
)
)
async def _get_or_create_device(self, zigpy_device):
"""Get or create a ZHA device."""
zha_device = self._devices.get(zigpy_device.ieee)
if zha_device is None:
zha_device = ZHADevice(self._hass, zigpy_device, self)
self._devices[zigpy_device.ieee] = zha_device
return zha_device
async def accept_zigbee_messages(self, _service_or_event):
"""Allow devices to accept zigbee messages."""
accept_messages_calls = []
for device in self.devices.values():
accept_messages_calls.append(device.async_accept_messages())
await asyncio.gather(*accept_messages_calls)
async def async_device_initialized(self, device, is_new_join):
"""Handle device joined and basic information discovered (async)."""
zha_device = await self._get_or_create_device(device)
discovery_infos = []
endpoint_tasks = []
for endpoint_id, endpoint in device.endpoints.items():
endpoint_tasks.append(self._async_process_endpoint(
endpoint_id, endpoint, discovery_infos, device, zha_device,
is_new_join
))
await asyncio.gather(*endpoint_tasks)
await zha_device.async_initialize(not is_new_join)
discovery_tasks = []
for discovery_info in discovery_infos:
discovery_tasks.append(_dispatch_discovery_info(
self._hass,
is_new_join,
discovery_info
))
await asyncio.gather(*discovery_tasks)
device_entity = _create_device_entity(zha_device)
await self._component.async_add_entities([device_entity])
async def _async_process_endpoint(
self, endpoint_id, endpoint, discovery_infos, device, zha_device,
is_new_join):
"""Process an endpoint on a zigpy device."""
import zigpy.profiles
if endpoint_id == 0: # ZDO
await _create_cluster_listener(
endpoint,
zha_device,
is_new_join,
listener_class=ZDOListener
)
return
component = None
profile_clusters = ([], [])
device_key = "{}-{}".format(device.ieee, endpoint_id)
node_config = {}
if CONF_DEVICE_CONFIG in self._config:
node_config = self._config[CONF_DEVICE_CONFIG].get(
device_key, {}
)
if endpoint.profile_id in zigpy.profiles.PROFILES:
profile = zigpy.profiles.PROFILES[endpoint.profile_id]
if zha_const.DEVICE_CLASS.get(endpoint.profile_id,
{}).get(endpoint.device_type,
None):
profile_clusters = profile.CLUSTERS[endpoint.device_type]
profile_info = zha_const.DEVICE_CLASS[endpoint.profile_id]
component = profile_info[endpoint.device_type]
if ha_const.CONF_TYPE in node_config:
component = node_config[ha_const.CONF_TYPE]
profile_clusters = zha_const.COMPONENT_CLUSTERS[component]
if component and component in COMPONENTS:
profile_match = await _handle_profile_match(
self._hass, endpoint, profile_clusters, zha_device,
component, device_key, is_new_join)
discovery_infos.append(profile_match)
discovery_infos.extend(await _handle_single_cluster_matches(
self._hass,
endpoint,
zha_device,
profile_clusters,
device_key,
is_new_join
))
async def _create_cluster_listener(cluster, zha_device, is_new_join,
listeners=None, listener_class=None):
"""Create a cluster listener and attach it to a device."""
if listener_class is None:
listener_class = LISTENER_REGISTRY.get(cluster.cluster_id,
AttributeListener)
listener = listener_class(cluster, zha_device)
if is_new_join:
await listener.async_configure()
zha_device.add_cluster_listener(listener)
if listeners is not None:
listeners.append(listener)
async def _dispatch_discovery_info(hass, is_new_join, discovery_info):
"""Dispatch or store discovery information."""
component = discovery_info['component']
if is_new_join:
async_dispatcher_send(
hass,
ZHA_DISCOVERY_NEW.format(component),
discovery_info
)
else:
hass.data[DATA_ZHA][component][discovery_info['unique_id']] = \
discovery_info
async def _handle_profile_match(hass, endpoint, profile_clusters, zha_device,
component, device_key, is_new_join):
"""Dispatch a profile match to the appropriate HA component."""
in_clusters = [endpoint.in_clusters[c]
for c in profile_clusters[0]
if c in endpoint.in_clusters]
out_clusters = [endpoint.out_clusters[c]
for c in profile_clusters[1]
if c in endpoint.out_clusters]
listeners = []
cluster_tasks = []
for cluster in in_clusters:
cluster_tasks.append(_create_cluster_listener(
cluster, zha_device, is_new_join, listeners=listeners))
for cluster in out_clusters:
cluster_tasks.append(_create_cluster_listener(
cluster, zha_device, is_new_join, listeners=listeners))
await asyncio.gather(*cluster_tasks)
discovery_info = {
'unique_id': device_key,
'zha_device': zha_device,
'listeners': listeners,
'component': component
}
if component == 'binary_sensor':
discovery_info.update({SENSOR_TYPE: UNKNOWN})
cluster_ids = []
cluster_ids.extend(profile_clusters[0])
cluster_ids.extend(profile_clusters[1])
for cluster_id in cluster_ids:
if cluster_id in BINARY_SENSOR_TYPES:
discovery_info.update({
SENSOR_TYPE: BINARY_SENSOR_TYPES.get(
cluster_id, UNKNOWN)
})
break
return discovery_info
async def _handle_single_cluster_matches(hass, endpoint, zha_device,
profile_clusters, device_key,
is_new_join):
"""Dispatch single cluster matches to HA components."""
cluster_matches = []
cluster_match_tasks = []
event_listener_tasks = []
for cluster in endpoint.in_clusters.values():
if cluster.cluster_id not in profile_clusters[0]:
cluster_match_tasks.append(_handle_single_cluster_match(
hass,
zha_device,
cluster,
device_key,
zha_const.SINGLE_INPUT_CLUSTER_DEVICE_CLASS,
is_new_join,
))
for cluster in endpoint.out_clusters.values():
if cluster.cluster_id not in profile_clusters[1]:
cluster_match_tasks.append(_handle_single_cluster_match(
hass,
zha_device,
cluster,
device_key,
zha_const.SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS,
is_new_join,
))
if cluster.cluster_id in EVENT_RELAY_CLUSTERS:
event_listener_tasks.append(_create_cluster_listener(
cluster,
zha_device,
is_new_join,
listener_class=EventRelayListener
))
await asyncio.gather(*event_listener_tasks)
cluster_match_results = await asyncio.gather(*cluster_match_tasks)
for cluster_match in cluster_match_results:
if cluster_match is not None:
cluster_matches.append(cluster_match)
return cluster_matches
async def _handle_single_cluster_match(hass, zha_device, cluster, device_key,
device_classes, is_new_join):
"""Dispatch a single cluster match to a HA component."""
component = None # sub_component = None
for cluster_type, candidate_component in device_classes.items():
if isinstance(cluster, cluster_type):
component = candidate_component
break
if component is None or component not in COMPONENTS:
return
listeners = []
await _create_cluster_listener(cluster, zha_device, is_new_join,
listeners=listeners)
# don't actually create entities for PowerConfiguration
# find a better way to do this without abusing single cluster reg
from zigpy.zcl.clusters.general import PowerConfiguration
if cluster.cluster_id == PowerConfiguration.cluster_id:
return
cluster_key = "{}-{}".format(device_key, cluster.cluster_id)
discovery_info = {
'unique_id': cluster_key,
'zha_device': zha_device,
'listeners': listeners,
'entity_suffix': '_{}'.format(cluster.cluster_id),
'component': component
}
if component == 'sensor':
discovery_info.update({
SENSOR_TYPE: SENSOR_TYPES.get(cluster.cluster_id, GENERIC)
})
if component == 'binary_sensor':
discovery_info.update({
SENSOR_TYPE: BINARY_SENSOR_TYPES.get(cluster.cluster_id, UNKNOWN)
})
return discovery_info
def _create_device_entity(zha_device):
"""Create ZHADeviceEntity."""
device_entity_listeners = []
if LISTENER_BATTERY in zha_device.cluster_listeners:
listener = zha_device.cluster_listeners.get(LISTENER_BATTERY)
device_entity_listeners.append(listener)
return ZhaDeviceEntity(zha_device, device_entity_listeners)
def establish_device_mappings(): def establish_device_mappings():
@ -266,19 +386,16 @@ def establish_device_mappings():
These cannot be module level, as importing bellows must be done in a These cannot be module level, as importing bellows must be done in a
in a function. in a function.
""" """
from zigpy import zcl, quirks from zigpy import zcl
from zigpy.profiles import PROFILES, zha, zll from zigpy.profiles import PROFILES, zha, zll
from ..sensor import RelativeHumiditySensor
if zha.PROFILE_ID not in DEVICE_CLASS: if zha.PROFILE_ID not in DEVICE_CLASS:
DEVICE_CLASS[zha.PROFILE_ID] = {} DEVICE_CLASS[zha.PROFILE_ID] = {}
if zll.PROFILE_ID not in DEVICE_CLASS: if zll.PROFILE_ID not in DEVICE_CLASS:
DEVICE_CLASS[zll.PROFILE_ID] = {} DEVICE_CLASS[zll.PROFILE_ID] = {}
EVENTABLE_CLUSTERS.append(zcl.clusters.general.AnalogInput.cluster_id) EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id)
EVENTABLE_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id) EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id)
EVENTABLE_CLUSTERS.append(zcl.clusters.general.MultistateInput.cluster_id)
EVENTABLE_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id)
DEVICE_CLASS[zha.PROFILE_ID].update({ DEVICE_CLASS[zha.PROFILE_ID].update({
zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor', zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor',
@ -293,6 +410,7 @@ def establish_device_mappings():
zha.DeviceType.DIMMER_SWITCH: 'binary_sensor', zha.DeviceType.DIMMER_SWITCH: 'binary_sensor',
zha.DeviceType.COLOR_DIMMER_SWITCH: 'binary_sensor', zha.DeviceType.COLOR_DIMMER_SWITCH: 'binary_sensor',
}) })
DEVICE_CLASS[zll.PROFILE_ID].update({ DEVICE_CLASS[zll.PROFILE_ID].update({
zll.DeviceType.ON_OFF_LIGHT: 'light', zll.DeviceType.ON_OFF_LIGHT: 'light',
zll.DeviceType.ON_OFF_PLUGIN_UNIT: 'switch', zll.DeviceType.ON_OFF_PLUGIN_UNIT: 'switch',
@ -321,14 +439,97 @@ def establish_device_mappings():
zcl.clusters.measurement.OccupancySensing: 'binary_sensor', zcl.clusters.measurement.OccupancySensing: 'binary_sensor',
zcl.clusters.hvac.Fan: 'fan', zcl.clusters.hvac.Fan: 'fan',
}) })
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({ SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({
zcl.clusters.general.OnOff: 'binary_sensor', zcl.clusters.general.OnOff: 'binary_sensor',
}) })
# A map of device/cluster to component/sub-component SENSOR_TYPES.update({
CUSTOM_CLUSTER_MAPPINGS.update({ zcl.clusters.measurement.RelativeHumidity.cluster_id: HUMIDITY,
(quirks.smartthings.SmartthingsTemperatureHumiditySensor, 64581): zcl.clusters.measurement.TemperatureMeasurement.cluster_id:
('sensor', RelativeHumiditySensor) TEMPERATURE,
zcl.clusters.measurement.PressureMeasurement.cluster_id: PRESSURE,
zcl.clusters.measurement.IlluminanceMeasurement.cluster_id:
ILLUMINANCE,
zcl.clusters.smartenergy.Metering.cluster_id: METERING,
zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id:
ELECTRICAL_MEASUREMENT,
zcl.clusters.general.PowerConfiguration.cluster_id:
POWER_CONFIGURATION,
})
BINARY_SENSOR_TYPES.update({
zcl.clusters.measurement.OccupancySensing.cluster_id: OCCUPANCY,
zcl.clusters.security.IasZone.cluster_id: ZONE,
zcl.clusters.general.OnOff.cluster_id: OPENING
})
CLUSTER_REPORT_CONFIGS.update({
zcl.clusters.general.OnOff.cluster_id: [{
'attr': 'on_off',
'config': REPORT_CONFIG_IMMEDIATE
}],
zcl.clusters.general.LevelControl.cluster_id: [{
'attr': 'current_level',
'config': REPORT_CONFIG_ASAP
}],
zcl.clusters.lighting.Color.cluster_id: [{
'attr': 'current_x',
'config': REPORT_CONFIG_DEFAULT
}, {
'attr': 'current_y',
'config': REPORT_CONFIG_DEFAULT
}, {
'attr': 'color_temperature',
'config': REPORT_CONFIG_DEFAULT
}],
zcl.clusters.measurement.RelativeHumidity.cluster_id: [{
'attr': 'measured_value',
'config': (
REPORT_CONFIG_MIN_INT,
REPORT_CONFIG_MAX_INT,
50
)
}],
zcl.clusters.measurement.TemperatureMeasurement.cluster_id: [{
'attr': 'measured_value',
'config': (
REPORT_CONFIG_MIN_INT,
REPORT_CONFIG_MAX_INT,
50
)
}],
zcl.clusters.measurement.PressureMeasurement.cluster_id: [{
'attr': 'measured_value',
'config': REPORT_CONFIG_DEFAULT
}],
zcl.clusters.measurement.IlluminanceMeasurement.cluster_id: [{
'attr': 'measured_value',
'config': REPORT_CONFIG_DEFAULT
}],
zcl.clusters.smartenergy.Metering.cluster_id: [{
'attr': 'instantaneous_demand',
'config': REPORT_CONFIG_DEFAULT
}],
zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: [{
'attr': 'active_power',
'config': REPORT_CONFIG_DEFAULT
}],
zcl.clusters.general.PowerConfiguration.cluster_id: [{
'attr': 'battery_voltage',
'config': REPORT_CONFIG_DEFAULT
}, {
'attr': 'battery_percentage_remaining',
'config': REPORT_CONFIG_DEFAULT
}],
zcl.clusters.measurement.OccupancySensing.cluster_id: [{
'attr': 'occupancy',
'config': REPORT_CONFIG_IMMEDIATE
}],
zcl.clusters.hvac.Fan.cluster_id: [{
'attr': 'fan_mode',
'config': REPORT_CONFIG_OP
}],
}) })
# A map of hass components to all Zigbee clusters it could use # A map of hass components to all Zigbee clusters it could use

View File

@ -5,20 +5,48 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/ https://home-assistant.io/components/zha/
""" """
import asyncio
from enum import Enum
from functools import wraps
import logging import logging
from random import uniform
from homeassistant.core import callback from homeassistant.core import callback
from .const import SIGNAL_ATTR_UPDATED from homeassistant.helpers.dispatcher import async_dispatcher_send
from .helpers import (
bind_configure_reporting, construct_unique_id,
safe_read, get_attr_id_by_name)
from .const import (
CLUSTER_REPORT_CONFIGS, REPORT_CONFIG_DEFAULT, SIGNAL_ATTR_UPDATED,
SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL, SIGNAL_STATE_ATTR, ATTR_LEVEL
)
LISTENER_REGISTRY = {}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def parse_and_log_command(entity_id, cluster, tsn, command_id, args): def populate_listener_registry():
"""Populate the listener registry."""
from zigpy import zcl
LISTENER_REGISTRY.update({
zcl.clusters.general.OnOff.cluster_id: OnOffListener,
zcl.clusters.general.LevelControl.cluster_id: LevelListener,
zcl.clusters.lighting.Color.cluster_id: ColorListener,
zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id:
ActivePowerListener,
zcl.clusters.general.PowerConfiguration.cluster_id: BatteryListener,
zcl.clusters.security.IasZone.cluster_id: IASZoneListener,
zcl.clusters.hvac.Fan.cluster_id: FanListener,
})
def parse_and_log_command(unique_id, cluster, tsn, command_id, args):
"""Parse and log a zigbee cluster command.""" """Parse and log a zigbee cluster command."""
cmd = cluster.server_commands.get(command_id, [command_id])[0] cmd = cluster.server_commands.get(command_id, [command_id])[0]
_LOGGER.debug( _LOGGER.debug(
"%s: received '%s' command with %s args on cluster_id '%s' tsn '%s'", "%s: received '%s' command with %s args on cluster_id '%s' tsn '%s'",
entity_id, unique_id,
cmd, cmd,
args, args,
cluster.cluster_id, cluster.cluster_id,
@ -27,40 +55,214 @@ def parse_and_log_command(entity_id, cluster, tsn, command_id, args):
return cmd return cmd
def decorate_command(listener, command):
"""Wrap a cluster command to make it safe."""
@wraps(command)
async def wrapper(*args, **kwds):
from zigpy.zcl.foundation import Status
from zigpy.exceptions import DeliveryError
try:
result = await command(*args, **kwds)
_LOGGER.debug("%s: executed command: %s %s %s %s",
listener.unique_id,
command.__name__,
"{}: {}".format("with args", args),
"{}: {}".format("with kwargs", kwds),
"{}: {}".format("and result", result))
return result[1] is Status.SUCCESS
except DeliveryError:
_LOGGER.debug("%s: command failed: %s", listener.unique_id,
command.__name__)
return False
return wrapper
class ListenerStatus(Enum):
"""Status of a listener."""
CREATED = 1
CONFIGURED = 2
INITIALIZED = 3
LISTENING = 4
class ClusterListener: class ClusterListener:
"""Listener for a Zigbee cluster.""" """Listener for a Zigbee cluster."""
def __init__(self, entity, cluster): def __init__(self, cluster, device):
"""Initialize ClusterListener.""" """Initialize ClusterListener."""
self._entity = entity
self._cluster = cluster self._cluster = cluster
self._zha_device = device
self._unique_id = construct_unique_id(cluster)
self._report_config = CLUSTER_REPORT_CONFIGS.get(
self._cluster.cluster_id,
[{'attr': 0, 'config': REPORT_CONFIG_DEFAULT}]
)
self._status = ListenerStatus.CREATED
@property
def unique_id(self):
"""Return the unique id for this listener."""
return self._unique_id
@property
def cluster(self):
"""Return the zigpy cluster for this listener."""
return self._cluster
@property
def device(self):
"""Return the device this listener is linked to."""
return self._zha_device
@property
def status(self):
"""Return the status of the listener."""
return self._status
def set_report_config(self, report_config):
"""Set the reporting configuration."""
self._report_config = report_config
async def async_configure(self):
"""Set cluster binding and attribute reporting."""
manufacturer = None
manufacturer_code = self._zha_device.manufacturer_code
if self.cluster.cluster_id >= 0xfc00 and manufacturer_code:
manufacturer = manufacturer_code
skip_bind = False # bind cluster only for the 1st configured attr
for report_config in self._report_config:
attr = report_config.get('attr')
min_report_interval, max_report_interval, change = \
report_config.get('config')
await bind_configure_reporting(
self._unique_id, self.cluster, attr,
min_report=min_report_interval,
max_report=max_report_interval,
reportable_change=change,
skip_bind=skip_bind,
manufacturer=manufacturer
)
skip_bind = True
await asyncio.sleep(uniform(0.1, 0.5))
_LOGGER.debug(
"%s: finished listener configuration",
self._unique_id
)
self._status = ListenerStatus.CONFIGURED
async def async_initialize(self, from_cache):
"""Initialize listener."""
self._status = ListenerStatus.INITIALIZED
async def accept_messages(self):
"""Attach to the cluster so we can receive messages."""
self._cluster.add_listener(self)
self._status = ListenerStatus.LISTENING
@callback
def cluster_command(self, tsn, command_id, args): def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster.""" """Handle commands received to this cluster."""
pass pass
@callback
def attribute_updated(self, attrid, value): def attribute_updated(self, attrid, value):
"""Handle attribute updates on this cluster.""" """Handle attribute updates on this cluster."""
pass pass
@callback
def zdo_command(self, *args, **kwargs): def zdo_command(self, *args, **kwargs):
"""Handle ZDO commands on this cluster.""" """Handle ZDO commands on this cluster."""
pass pass
@callback
def zha_send_event(self, cluster, command, args): def zha_send_event(self, cluster, command, args):
"""Relay entity events to hass.""" """Relay events to hass."""
pass # don't let entities fire events self._zha_device.hass.bus.async_fire(
'zha_event',
{
'unique_id': self._unique_id,
'command': command,
'args': args
}
)
async def async_update(self):
"""Retrieve latest state from cluster."""
pass
async def get_attribute_value(self, attribute, from_cache=True):
"""Get the value for an attribute."""
result = await safe_read(
self._cluster,
[attribute],
allow_cache=from_cache,
only_cache=from_cache
)
return result.get(attribute)
def __getattr__(self, name):
"""Get attribute or a decorated cluster command."""
if hasattr(self._cluster, name) and callable(
getattr(self._cluster, name)):
command = getattr(self._cluster, name)
command.__name__ = name
return decorate_command(
self,
command
)
return self.__getattribute__(name)
class AttributeListener(ClusterListener):
"""Listener for the attribute reports cluster."""
name = 'attribute'
def __init__(self, cluster, device):
"""Initialize AttributeListener."""
super().__init__(cluster, device)
attr = self._report_config[0].get('attr')
if isinstance(attr, str):
self._value_attribute = get_attr_id_by_name(self.cluster, attr)
else:
self._value_attribute = attr
@callback
def attribute_updated(self, attrid, value):
"""Handle attribute updates on this cluster."""
if attrid == self._value_attribute:
async_dispatcher_send(
self._zha_device.hass,
"{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED),
value
)
async def async_initialize(self, from_cache):
"""Initialize listener."""
await self.get_attribute_value(
self._report_config[0].get('attr'), from_cache=from_cache)
await super().async_initialize(from_cache)
class OnOffListener(ClusterListener): class OnOffListener(ClusterListener):
"""Listener for the OnOff Zigbee cluster.""" """Listener for the OnOff Zigbee cluster."""
name = 'on_off'
ON_OFF = 0 ON_OFF = 0
def __init__(self, cluster, device):
"""Initialize ClusterListener."""
super().__init__(cluster, device)
self._state = None
@callback
def cluster_command(self, tsn, command_id, args): def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster.""" """Handle commands received to this cluster."""
cmd = parse_and_log_command( cmd = parse_and_log_command(
self._entity.entity_id, self.unique_id,
self._cluster, self._cluster,
tsn, tsn,
command_id, command_id,
@ -68,27 +270,42 @@ class OnOffListener(ClusterListener):
) )
if cmd in ('off', 'off_with_effect'): if cmd in ('off', 'off_with_effect'):
self._entity.set_state(False) self.attribute_updated(self.ON_OFF, False)
elif cmd in ('on', 'on_with_recall_global_scene', 'on_with_timed_off'): elif cmd in ('on', 'on_with_recall_global_scene', 'on_with_timed_off'):
self._entity.set_state(True) self.attribute_updated(self.ON_OFF, True)
elif cmd == 'toggle': elif cmd == 'toggle':
self._entity.set_state(not self._entity.is_on) self.attribute_updated(self.ON_OFF, not bool(self._state))
@callback
def attribute_updated(self, attrid, value): def attribute_updated(self, attrid, value):
"""Handle attribute updates on this cluster.""" """Handle attribute updates on this cluster."""
if attrid == self.ON_OFF: if attrid == self.ON_OFF:
self._entity.set_state(bool(value)) async_dispatcher_send(
self._zha_device.hass,
"{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED),
value
)
self._state = bool(value)
async def async_initialize(self, from_cache):
"""Initialize listener."""
self._state = bool(
await self.get_attribute_value(self.ON_OFF, from_cache=from_cache))
await super().async_initialize(from_cache)
class LevelListener(ClusterListener): class LevelListener(ClusterListener):
"""Listener for the LevelControl Zigbee cluster.""" """Listener for the LevelControl Zigbee cluster."""
name = ATTR_LEVEL
CURRENT_LEVEL = 0 CURRENT_LEVEL = 0
@callback
def cluster_command(self, tsn, command_id, args): def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster.""" """Handle commands received to this cluster."""
cmd = parse_and_log_command( cmd = parse_and_log_command(
self._entity.entity_id, self.unique_id,
self._cluster, self._cluster,
tsn, tsn,
command_id, command_id,
@ -96,21 +313,190 @@ class LevelListener(ClusterListener):
) )
if cmd in ('move_to_level', 'move_to_level_with_on_off'): if cmd in ('move_to_level', 'move_to_level_with_on_off'):
self._entity.set_level(args[0]) self.dispatch_level_change(SIGNAL_SET_LEVEL, args[0])
elif cmd in ('move', 'move_with_on_off'): elif cmd in ('move', 'move_with_on_off'):
# We should dim slowly -- for now, just step once # We should dim slowly -- for now, just step once
rate = args[1] rate = args[1]
if args[0] == 0xff: if args[0] == 0xff:
rate = 10 # Should read default move rate rate = 10 # Should read default move rate
self._entity.move_level(-rate if args[0] else rate) self.dispatch_level_change(
SIGNAL_MOVE_LEVEL, -rate if args[0] else rate)
elif cmd in ('step', 'step_with_on_off'): elif cmd in ('step', 'step_with_on_off'):
# Step (technically may change on/off) # Step (technically may change on/off)
self._entity.move_level(-args[1] if args[0] else args[1]) self.dispatch_level_change(
SIGNAL_MOVE_LEVEL, -args[1] if args[0] else args[1])
@callback
def attribute_updated(self, attrid, value): def attribute_updated(self, attrid, value):
"""Handle attribute updates on this cluster.""" """Handle attribute updates on this cluster."""
_LOGGER.debug("%s: received attribute: %s update with value: %i",
self.unique_id, attrid, value)
if attrid == self.CURRENT_LEVEL: if attrid == self.CURRENT_LEVEL:
self._entity.set_level(value) self.dispatch_level_change(SIGNAL_SET_LEVEL, value)
def dispatch_level_change(self, command, level):
"""Dispatch level change."""
async_dispatcher_send(
self._zha_device.hass,
"{}_{}".format(self.unique_id, command),
level
)
async def async_initialize(self, from_cache):
"""Initialize listener."""
await self.get_attribute_value(
self.CURRENT_LEVEL, from_cache=from_cache)
await super().async_initialize(from_cache)
class IASZoneListener(ClusterListener):
"""Listener for the IASZone Zigbee cluster."""
name = 'zone'
def __init__(self, cluster, device):
"""Initialize IASZoneListener."""
super().__init__(cluster, device)
self._cluster.add_listener(self)
self._status = ListenerStatus.LISTENING
@callback
def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster."""
if command_id == 0:
state = args[0] & 3
async_dispatcher_send(
self._zha_device.hass,
"{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED),
state
)
_LOGGER.debug("Updated alarm state: %s", state)
elif command_id == 1:
_LOGGER.debug("Enroll requested")
res = self._cluster.enroll_response(0, 0)
self._zha_device.hass.async_create_task(res)
async def async_configure(self):
"""Configure IAS device."""
from zigpy.exceptions import DeliveryError
_LOGGER.debug("%s: started IASZoneListener configuration",
self._unique_id)
try:
res = await self._cluster.bind()
_LOGGER.debug(
"%s: bound '%s' cluster: %s",
self.unique_id, self._cluster.ep_attribute, res[0]
)
except DeliveryError as ex:
_LOGGER.debug(
"%s: Failed to bind '%s' cluster: %s",
self.unique_id, self._cluster.ep_attribute, str(ex)
)
ieee = self._cluster.endpoint.device.application.ieee
try:
res = await self._cluster.write_attributes({'cie_addr': ieee})
_LOGGER.debug(
"%s: wrote cie_addr: %s to '%s' cluster: %s",
self.unique_id, str(ieee), self._cluster.ep_attribute,
res[0]
)
except DeliveryError as ex:
_LOGGER.debug(
"%s: Failed to write cie_addr: %s to '%s' cluster: %s",
self.unique_id, str(ieee), self._cluster.ep_attribute, str(ex)
)
_LOGGER.debug("%s: finished IASZoneListener configuration",
self._unique_id)
await self.get_attribute_value('zone_type', from_cache=False)
@callback
def attribute_updated(self, attrid, value):
"""Handle attribute updates on this cluster."""
if attrid == 2:
value = value & 3
async_dispatcher_send(
self._zha_device.hass,
"{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED),
value
)
async def async_initialize(self, from_cache):
"""Initialize listener."""
await self.get_attribute_value('zone_status', from_cache=from_cache)
await self.get_attribute_value('zone_state', from_cache=from_cache)
await super().async_initialize(from_cache)
async def accept_messages(self):
"""Attach to the cluster so we can receive messages."""
self._status = ListenerStatus.LISTENING
class ActivePowerListener(AttributeListener):
"""Listener that polls active power level."""
name = 'active_power'
async def async_update(self):
"""Retrieve latest state."""
_LOGGER.debug("%s async_update", self.unique_id)
# This is a polling listener. Don't allow cache.
result = await self.get_attribute_value(
'active_power', from_cache=False)
async_dispatcher_send(
self._zha_device.hass,
"{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED),
result
)
async def async_initialize(self, from_cache):
"""Initialize listener."""
await self.get_attribute_value(
'active_power', from_cache=from_cache)
await super().async_initialize(from_cache)
class BatteryListener(ClusterListener):
"""Listener that polls active power level."""
name = 'battery'
@callback
def attribute_updated(self, attrid, value):
"""Handle attribute updates on this cluster."""
attr = self._report_config[1].get('attr')
if isinstance(attr, str):
attr_id = get_attr_id_by_name(self.cluster, attr)
else:
attr_id = attr
if attrid == attr_id:
async_dispatcher_send(
self._zha_device.hass,
"{}_{}".format(self.unique_id, SIGNAL_STATE_ATTR),
'battery_level',
value
)
async def async_initialize(self, from_cache):
"""Initialize listener."""
await self.async_read_state(from_cache)
await super().async_initialize(from_cache)
async def async_update(self):
"""Retrieve latest state."""
await self.async_read_state(True)
async def async_read_state(self, from_cache):
"""Read data from the cluster."""
await self.get_attribute_value(
'battery_size', from_cache=from_cache)
await self.get_attribute_value(
'battery_percentage_remaining', from_cache=from_cache)
await self.get_attribute_value(
'active_power', from_cache=from_cache)
class EventRelayListener(ClusterListener): class EventRelayListener(ClusterListener):
@ -143,3 +529,137 @@ class EventRelayListener(ClusterListener):
self._cluster.server_commands.get(command_id)[0], self._cluster.server_commands.get(command_id)[0],
args args
) )
class ColorListener(ClusterListener):
"""Color listener."""
name = 'color'
CAPABILITIES_COLOR_XY = 0x08
CAPABILITIES_COLOR_TEMP = 0x10
UNSUPPORTED_ATTRIBUTE = 0x86
def __init__(self, cluster, device):
"""Initialize ClusterListener."""
super().__init__(cluster, device)
self._color_capabilities = None
def get_color_capabilities(self):
"""Return the color capabilities."""
return self._color_capabilities
async def async_initialize(self, from_cache):
"""Initialize listener."""
capabilities = await self.get_attribute_value(
'color_capabilities', from_cache=from_cache)
if capabilities is None:
# ZCL Version 4 devices don't support the color_capabilities
# attribute. In this version XY support is mandatory, but we
# need to probe to determine if the device supports color
# temperature.
capabilities = self.CAPABILITIES_COLOR_XY
result = await self.get_attribute_value(
'color_temperature', from_cache=from_cache)
if result is not self.UNSUPPORTED_ATTRIBUTE:
capabilities |= self.CAPABILITIES_COLOR_TEMP
self._color_capabilities = capabilities
await super().async_initialize(from_cache)
class FanListener(ClusterListener):
"""Fan listener."""
name = 'fan'
_value_attribute = 0
async def async_set_speed(self, value) -> None:
"""Set the speed of the fan."""
from zigpy.exceptions import DeliveryError
try:
await self.cluster.write_attributes({'fan_mode': value})
except DeliveryError as ex:
_LOGGER.error("%s: Could not set speed: %s", self.unique_id, ex)
return
async def async_update(self):
"""Retrieve latest state."""
result = await self.get_attribute_value('fan_mode', from_cache=True)
async_dispatcher_send(
self._zha_device.hass,
"{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED),
result
)
def attribute_updated(self, attrid, value):
"""Handle attribute update from fan cluster."""
attr_name = self.cluster.attributes.get(attrid, [attrid])[0]
_LOGGER.debug("%s: Attribute report '%s'[%s] = %s",
self.unique_id, self.cluster.name, attr_name, value)
if attrid == self._value_attribute:
async_dispatcher_send(
self._zha_device.hass,
"{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED),
value
)
async def async_initialize(self, from_cache):
"""Initialize listener."""
await self.get_attribute_value(
self._value_attribute, from_cache=from_cache)
await super().async_initialize(from_cache)
class ZDOListener:
"""Listener for ZDO events."""
name = 'zdo'
def __init__(self, cluster, device):
"""Initialize ClusterListener."""
self._cluster = cluster
self._zha_device = device
self._status = ListenerStatus.CREATED
self._unique_id = "{}_ZDO".format(device.name)
@property
def unique_id(self):
"""Return the unique id for this listener."""
return self._unique_id
@property
def cluster(self):
"""Return the aigpy cluster for this listener."""
return self._cluster
@property
def status(self):
"""Return the status of the listener."""
return self._status
@callback
def device_announce(self, zigpy_device):
"""Device announce handler."""
pass
@callback
def permit_duration(self, duration):
"""Permit handler."""
pass
async def accept_messages(self):
"""Attach to the cluster so we can receive messages."""
self._cluster.add_listener(self)
self._status = ListenerStatus.LISTENING
async def async_initialize(self, from_cache):
"""Initialize listener."""
self._status = ListenerStatus.INITIALIZED
async def async_configure(self):
"""Configure listener."""
self._status = ListenerStatus.CONFIGURED

View File

@ -5,78 +5,134 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/ https://home-assistant.io/components/zha/
""" """
import logging
import time import time
from homeassistant.helpers import entity
from homeassistant.util import slugify from homeassistant.util import slugify
from .entity import ZhaEntity
from .const import LISTENER_BATTERY, SIGNAL_STATE_ATTR
_LOGGER = logging.getLogger(__name__)
BATTERY_SIZES = {
0: 'No battery',
1: 'Built in',
2: 'Other',
3: 'AA',
4: 'AAA',
5: 'C',
6: 'D',
7: 'CR2',
8: 'CR123A',
9: 'CR2450',
10: 'CR2032',
11: 'CR1632',
255: 'Unknown'
}
class ZhaDeviceEntity(entity.Entity): class ZhaDeviceEntity(ZhaEntity):
"""A base class for ZHA devices.""" """A base class for ZHA devices."""
def __init__(self, device, manufacturer, model, application_listener, def __init__(self, zha_device, listeners, keepalive_interval=7200,
keepalive_interval=7200, **kwargs): **kwargs):
"""Init ZHA endpoint entity.""" """Init ZHA endpoint entity."""
self._device_state_attributes = { ieee = zha_device.ieee
'nwk': '0x{0:04x}'.format(device.nwk),
'ieee': str(device.ieee),
'lqi': device.lqi,
'rssi': device.rssi,
}
ieee = device.ieee
ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]]) ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
if manufacturer is not None and model is not None: unique_id = None
self._unique_id = "{}_{}_{}".format( if zha_device.manufacturer is not None and \
slugify(manufacturer), zha_device.model is not None:
slugify(model), unique_id = "{}_{}_{}".format(
slugify(zha_device.manufacturer),
slugify(zha_device.model),
ieeetail, ieeetail,
) )
self._device_state_attributes['friendly_name'] = "{} {}".format(
manufacturer,
model,
)
else: else:
self._unique_id = str(ieeetail) unique_id = str(ieeetail)
kwargs['component'] = 'zha'
super().__init__(unique_id, zha_device, listeners, skip_entity_id=True,
**kwargs)
self._device = device
self._state = 'offline'
self._keepalive_interval = keepalive_interval self._keepalive_interval = keepalive_interval
self._device_state_attributes.update({
application_listener.register_entity(ieee, self) 'nwk': '0x{0:04x}'.format(zha_device.nwk),
'ieee': str(zha_device.ieee),
@property 'lqi': zha_device.lqi,
def unique_id(self) -> str: 'rssi': zha_device.rssi,
"""Return a unique ID.""" })
return self._unique_id self._should_poll = True
self._battery_listener = self.cluster_listeners.get(LISTENER_BATTERY)
@property @property
def state(self) -> str: def state(self) -> str:
"""Return the state of the entity.""" """Return the state of the entity."""
return self._state return self._state
@property
def available(self):
"""Return True if device is available."""
return self._zha_device.available
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return device specific state attributes.""" """Return device specific state attributes."""
update_time = None update_time = None
if self._device.last_seen is not None and self._state == 'offline': device = self._zha_device
time_struct = time.localtime(self._device.last_seen) if device.last_seen is not None and not self.available:
time_struct = time.localtime(device.last_seen)
update_time = time.strftime("%Y-%m-%dT%H:%M:%S", time_struct) update_time = time.strftime("%Y-%m-%dT%H:%M:%S", time_struct)
self._device_state_attributes['last_seen'] = update_time self._device_state_attributes['last_seen'] = update_time
if ('last_seen' in self._device_state_attributes and if ('last_seen' in self._device_state_attributes and
self._state != 'offline'): self.available):
del self._device_state_attributes['last_seen'] del self._device_state_attributes['last_seen']
self._device_state_attributes['lqi'] = self._device.lqi self._device_state_attributes['lqi'] = device.lqi
self._device_state_attributes['rssi'] = self._device.rssi self._device_state_attributes['rssi'] = device.rssi
return self._device_state_attributes return self._device_state_attributes
async def async_added_to_hass(self):
"""Run when about to be added to hass."""
await super().async_added_to_hass()
if self._battery_listener:
await self.async_accept_signal(
self._battery_listener, SIGNAL_STATE_ATTR,
self.async_update_state_attribute)
# only do this on add to HA because it is static
await self._async_init_battery_values()
async def async_update(self): async def async_update(self):
"""Handle polling.""" """Handle polling."""
if self._device.last_seen is None: if self._zha_device.last_seen is None:
self._state = 'offline' self._zha_device.update_available(False)
else: else:
difference = time.time() - self._device.last_seen difference = time.time() - self._zha_device.last_seen
if difference > self._keepalive_interval: if difference > self._keepalive_interval:
self._state = 'offline' self._zha_device.update_available(False)
self._state = None
else: else:
self._zha_device.update_available(True)
self._state = 'online' self._state = 'online'
if self._battery_listener:
await self.async_get_latest_battery_reading()
async def _async_init_battery_values(self):
"""Get initial battery level and battery info from listener cache."""
battery_size = await self._battery_listener.get_attribute_value(
'battery_size')
if battery_size is not None:
self._device_state_attributes['battery_size'] = BATTERY_SIZES.get(
battery_size, 'Unknown')
battery_quantity = await self._battery_listener.get_attribute_value(
'battery_quantity')
if battery_quantity is not None:
self._device_state_attributes['battery_quantity'] = \
battery_quantity
await self.async_get_latest_battery_reading()
async def async_get_latest_battery_reading(self):
"""Get the latest battery reading from listeners cache."""
battery = await self._battery_listener.get_attribute_value(
'battery_percentage_remaining')
if battery is not None:
self._device_state_attributes['battery_level'] = battery

View File

@ -4,20 +4,18 @@ Entity for Zigbee Home Automation.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/ https://home-assistant.io/components/zha/
""" """
import asyncio
import logging
from random import uniform
from homeassistant.const import ATTR_ENTITY_ID import logging
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
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import slugify from homeassistant.util import slugify
from .core.const import ( from .core.const import (
DATA_ZHA, DATA_ZHA_BRIDGE_ID, DOMAIN, ATTR_CLUSTER_ID, ATTR_ATTRIBUTE, DOMAIN, ATTR_MANUFACTURER, DATA_ZHA, DATA_ZHA_BRIDGE_ID, MODEL, NAME,
ATTR_VALUE, ATTR_MANUFACTURER, ATTR_COMMAND, SERVER, ATTR_COMMAND_TYPE, SIGNAL_REMOVE
ATTR_ARGS, IN, OUT, CLIENT_COMMANDS, SERVER_COMMANDS) )
from .core.helpers import bind_configure_reporting
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -29,287 +27,155 @@ class ZhaEntity(entity.Entity):
_domain = None # Must be overridden by subclasses _domain = None # Must be overridden by subclasses
def __init__(self, endpoint, in_clusters, out_clusters, manufacturer, def __init__(self, unique_id, zha_device, listeners,
model, application_listener, unique_id, new_join=False, skip_entity_id=False, **kwargs):
**kwargs):
"""Init ZHA entity.""" """Init ZHA entity."""
self._device_state_attributes = {} self._force_update = False
self._name = None self._should_poll = False
ieee = endpoint.device.ieee
ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
if manufacturer and model is not None:
self.entity_id = "{}.{}_{}_{}_{}{}".format(
self._domain,
slugify(manufacturer),
slugify(model),
ieeetail,
endpoint.endpoint_id,
kwargs.get(ENTITY_SUFFIX, ''),
)
self._name = "{} {}".format(manufacturer, model)
else:
self.entity_id = "{}.zha_{}_{}{}".format(
self._domain,
ieeetail,
endpoint.endpoint_id,
kwargs.get(ENTITY_SUFFIX, ''),
)
self._endpoint = endpoint
self._in_clusters = in_clusters
self._out_clusters = out_clusters
self._new_join = new_join
self._state = None
self._unique_id = unique_id self._unique_id = unique_id
self._name = None
# Normally the entity itself is the listener. Sub-classes may set this if zha_device.manufacturer and zha_device.model is not None:
# to a dict of cluster ID -> listener to receive messages for specific self._name = "{} {}".format(
# clusters separately zha_device.manufacturer,
self._in_listeners = {} zha_device.model
self._out_listeners = {}
self._initialized = False
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( if not skip_entity_id:
'set: %s for attr: %s to cluster: %s for entity: %s - res: %s', ieee = zha_device.ieee
value, ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
attribute, if zha_device.manufacturer and zha_device.model is not None:
cluster_id, self.entity_id = "{}.{}_{}_{}_{}{}".format(
self.entity_id, self._domain,
response slugify(zha_device.manufacturer),
) slugify(zha_device.model),
return response ieeetail,
except DeliveryError as exc: listeners[0].cluster.endpoint.endpoint_id,
_LOGGER.debug( kwargs.get(ENTITY_SUFFIX, ''),
'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.
It is now safe to update the entity state
"""
for cluster_id, cluster in self._in_clusters.items():
cluster.add_listener(self._in_listeners.get(cluster_id, self))
for cluster_id, cluster in self._out_clusters.items():
cluster.add_listener(self._out_listeners.get(cluster_id, self))
self._endpoint.device.zdo.add_listener(self)
if self._new_join:
self.hass.async_create_task(self.async_configure())
self._initialized = True
async def async_configure(self):
"""Set cluster binding and attribute reporting."""
for cluster_key, attrs in self.zcl_reporting_config.items():
cluster = self._get_cluster_from_report_config(cluster_key)
if cluster is None:
continue
manufacturer = None
if cluster.cluster_id >= 0xfc00 and self.manufacturer_code:
manufacturer = self.manufacturer_code
skip_bind = False # bind cluster only for the 1st configured attr
for attr, details in attrs.items():
min_report_interval, max_report_interval, change = details
await bind_configure_reporting(
self.entity_id, cluster, attr,
min_report=min_report_interval,
max_report=max_report_interval,
reportable_change=change,
skip_bind=skip_bind,
manufacturer=manufacturer
) )
skip_bind = True else:
await asyncio.sleep(uniform(0.1, 0.5)) self.entity_id = "{}.zha_{}_{}{}".format(
_LOGGER.debug("%s: finished configuration", self.entity_id) self._domain,
ieeetail,
def _get_cluster_from_report_config(self, cluster_key): listeners[0].cluster.endpoint.endpoint_id,
"""Parse an entry from zcl_reporting_config dict.""" kwargs.get(ENTITY_SUFFIX, ''),
from zigpy.zcl import Cluster as Zcl_Cluster )
self._state = None
cluster = None self._device_state_attributes = {}
if isinstance(cluster_key, Zcl_Cluster): self._zha_device = zha_device
cluster = cluster_key self.cluster_listeners = {}
elif isinstance(cluster_key, str): # this will get flipped to false once we enable the feature after the
cluster = getattr(self._endpoint, cluster_key, None) # reorg is merged
elif isinstance(cluster_key, int): self._available = True
if cluster_key in self._in_clusters: self._component = kwargs['component']
cluster = self._in_clusters[cluster_key] self._unsubs = []
elif cluster_key in self._out_clusters: for listener in listeners:
cluster = self._out_clusters[cluster_key] self.cluster_listeners[listener.name] = listener
elif issubclass(cluster_key, Zcl_Cluster):
cluster_id = cluster_key.cluster_id
if cluster_id in self._in_clusters:
cluster = self._in_clusters[cluster_id]
elif cluster_id in self._out_clusters:
cluster = self._out_clusters[cluster_id]
return cluster
@property @property
def name(self): def name(self):
"""Return Entity's default name.""" """Return Entity's default name."""
return self._name return self._name
@property
def zcl_reporting_config(self):
"""Return a dict of ZCL attribute reporting configuration.
{
Cluster_Class: {
attr_id: (min_report_interval, max_report_interval, change),
attr_name: (min_rep_interval, max_rep_interval, change)
}
Cluster_Instance: {
attr_id: (min_report_interval, max_report_interval, change),
attr_name: (min_rep_interval, max_rep_interval, change)
}
cluster_id: {
attr_id: (min_report_interval, max_report_interval, change),
attr_name: (min_rep_interval, max_rep_interval, change)
}
'cluster_name': {
attr_id: (min_report_interval, max_report_interval, change),
attr_name: (min_rep_interval, max_rep_interval, change)
}
}
"""
return {}
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Return a unique ID.""" """Return a unique ID."""
return self._unique_id return self._unique_id
@property
def zha_device(self):
"""Return the zha device this entity is attached to."""
return self._zha_device
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return device specific state attributes.""" """Return device specific state attributes."""
return self._device_state_attributes return self._device_state_attributes
@property
def force_update(self) -> bool:
"""Force update this entity."""
return self._force_update
@property @property
def should_poll(self) -> bool: def should_poll(self) -> bool:
"""Let ZHA handle polling.""" """Poll state from device."""
return False return self._should_poll
@callback
def attribute_updated(self, attribute, value):
"""Handle an attribute updated on this cluster."""
pass
@callback
def zdo_command(self, tsn, command_id, args):
"""Handle a ZDO command received on this cluster."""
pass
@callback
def device_announce(self, device):
"""Handle device_announce zdo event."""
self.async_schedule_update_ha_state(force_refresh=True)
@callback
def permit_duration(self, permit_duration):
"""Handle permit_duration zdo event."""
pass
@property @property
def device_info(self): def device_info(self):
"""Return a device description for device registry.""" """Return a device description for device registry."""
ieee = str(self._endpoint.device.ieee) zha_device_info = self._zha_device.device_info
ieee = zha_device_info['ieee']
return { return {
'connections': {(CONNECTION_ZIGBEE, ieee)}, 'connections': {(CONNECTION_ZIGBEE, ieee)},
'identifiers': {(DOMAIN, ieee)}, 'identifiers': {(DOMAIN, ieee)},
ATTR_MANUFACTURER: self._endpoint.manufacturer, ATTR_MANUFACTURER: zha_device_info[ATTR_MANUFACTURER],
'model': self._endpoint.model, MODEL: zha_device_info[MODEL],
'name': self.name or ieee, NAME: zha_device_info[NAME],
'via_hub': (DOMAIN, self.hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID]), 'via_hub': (DOMAIN, self.hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID]),
} }
@callback @property
def zha_send_event(self, cluster, command, args): def available(self):
"""Relay entity events to hass.""" """Return entity availability."""
pass # don't relay events from entities return self._available
def async_set_available(self, available):
"""Set entity availability."""
self._available = available
self.async_schedule_update_ha_state()
def async_update_state_attribute(self, key, value):
"""Update a single device state attribute."""
self._device_state_attributes.update({
key: value
})
self.async_schedule_update_ha_state()
def async_set_state(self, state):
"""Set the entity state."""
pass
async def async_added_to_hass(self):
"""Run when about to be added to hass."""
await super().async_added_to_hass()
await self.async_accept_signal(
None, "{}_{}".format(self.zha_device.available_signal, 'entity'),
self.async_set_available,
signal_override=True)
await self.async_accept_signal(
None, "{}_{}".format(SIGNAL_REMOVE, str(self.zha_device.ieee)),
self.async_remove,
signal_override=True
)
self._zha_device.gateway.register_entity_reference(
self._zha_device.ieee, self.entity_id, self._zha_device,
self.cluster_listeners, self.device_info)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect entity object when removed."""
for unsub in self._unsubs:
unsub()
async def async_update(self):
"""Retrieve latest state."""
for listener in self.cluster_listeners:
if hasattr(listener, 'async_update'):
await listener.async_update()
async def async_accept_signal(self, listener, signal, func,
signal_override=False):
"""Accept a signal from a listener."""
unsub = None
if signal_override:
unsub = async_dispatcher_connect(
self.hass,
signal,
func
)
else:
unsub = async_dispatcher_connect(
self.hass,
"{}_{}".format(listener.unique_id, signal),
func
)
self._unsubs.append(unsub)

View File

@ -1,99 +0,0 @@
"""
Event for Zigbee Home Automation.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/
"""
import logging
from homeassistant.core import EventOrigin, callback
from homeassistant.util import slugify
_LOGGER = logging.getLogger(__name__)
class ZhaEvent():
"""A base class for ZHA events."""
def __init__(self, hass, cluster, **kwargs):
"""Init ZHA event."""
self._hass = hass
self._cluster = cluster
cluster.add_listener(self)
ieee = cluster.endpoint.device.ieee
ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
endpoint = cluster.endpoint
if endpoint.manufacturer and endpoint.model is not None:
self._unique_id = "{}.{}_{}_{}_{}{}".format(
'zha_event',
slugify(endpoint.manufacturer),
slugify(endpoint.model),
ieeetail,
cluster.endpoint.endpoint_id,
kwargs.get('entity_suffix', ''),
)
else:
self._unique_id = "{}.zha_{}_{}{}".format(
'zha_event',
ieeetail,
cluster.endpoint.endpoint_id,
kwargs.get('entity_suffix', ''),
)
@callback
def attribute_updated(self, attribute, value):
"""Handle an attribute updated on this cluster."""
pass
@callback
def zdo_command(self, tsn, command_id, args):
"""Handle a ZDO command received on this cluster."""
pass
@callback
def cluster_command(self, tsn, command_id, args):
"""Handle a cluster command received on this cluster."""
pass
@callback
def zha_send_event(self, cluster, command, args):
"""Relay entity events to hass."""
self._hass.bus.async_fire(
'zha_event',
{
'unique_id': self._unique_id,
'command': command,
'args': args
},
EventOrigin.remote
)
class ZhaRelayEvent(ZhaEvent):
"""Event relay that can be attached to zigbee clusters."""
@callback
def attribute_updated(self, attribute, value):
"""Handle an attribute updated on this cluster."""
self.zha_send_event(
self._cluster,
'attribute_updated',
{
'attribute_id': attribute,
'attribute_name': self._cluster.attributes.get(
attribute,
['Unknown'])[0],
'value': value
}
)
@callback
def cluster_command(self, tsn, command_id, args):
"""Handle a cluster command received on this cluster."""
if self._cluster.server_commands is not None and\
self._cluster.server_commands.get(command_id) is not None:
self.zha_send_event(
self._cluster,
self._cluster.server_commands.get(command_id)[0],
args
)

View File

@ -10,9 +10,10 @@ from homeassistant.components.fan import (
DOMAIN, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED, DOMAIN, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED,
FanEntity) FanEntity)
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .core import helpers
from .core.const import ( from .core.const import (
DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_OP, ZHA_DISCOVERY_NEW) DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, LISTENER_FAN,
SIGNAL_ATTR_UPDATED
)
from .entity import ZhaEntity from .entity import ZhaEntity
DEPENDENCIES = ['zha'] DEPENDENCIES = ['zha']
@ -79,19 +80,17 @@ class ZhaFan(ZhaEntity, FanEntity):
"""Representation of a ZHA fan.""" """Representation of a ZHA fan."""
_domain = DOMAIN _domain = DOMAIN
value_attribute = 0 # fan_mode
@property def __init__(self, unique_id, zha_device, listeners, **kwargs):
def zcl_reporting_config(self) -> dict: """Init this sensor."""
"""Return a dict of attribute reporting configuration.""" super().__init__(unique_id, zha_device, listeners, **kwargs)
return { self._fan_listener = self.cluster_listeners.get(LISTENER_FAN)
self.cluster: {self.value_attribute: REPORT_CONFIG_OP}
}
@property async def async_added_to_hass(self):
def cluster(self): """Run when about to be added to hass."""
"""Fan ZCL Cluster.""" await super().async_added_to_hass()
return self._endpoint.fan await self.async_accept_signal(
self._fan_listener, SIGNAL_ATTR_UPDATED, self.async_set_state)
@property @property
def supported_features(self) -> int: def supported_features(self) -> int:
@ -115,6 +114,16 @@ class ZhaFan(ZhaEntity, FanEntity):
return False return False
return self._state != SPEED_OFF return self._state != SPEED_OFF
@property
def device_state_attributes(self):
"""Return state attributes."""
return self.state_attributes
def async_set_state(self, state):
"""Handle state update from listener."""
self._state = VALUE_TO_SPEED.get(state, self._state)
self.async_schedule_update_ha_state()
async def async_turn_on(self, speed: str = None, **kwargs) -> None: async def async_turn_on(self, speed: str = None, **kwargs) -> None:
"""Turn the entity on.""" """Turn the entity on."""
if speed is None: if speed is None:
@ -128,31 +137,5 @@ class ZhaFan(ZhaEntity, FanEntity):
async def async_set_speed(self, speed: str) -> None: async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan.""" """Set the speed of the fan."""
from zigpy.exceptions import DeliveryError await self._fan_listener.async_set_speed(SPEED_TO_VALUE[speed])
try: self.async_set_state(speed)
await self._endpoint.fan.write_attributes(
{'fan_mode': SPEED_TO_VALUE[speed]}
)
except DeliveryError as ex:
_LOGGER.error("%s: Could not set speed: %s", self.entity_id, ex)
return
self._state = speed
self.async_schedule_update_ha_state()
async def async_update(self):
"""Retrieve latest state."""
result = await helpers.safe_read(self.cluster, ['fan_mode'],
allow_cache=False,
only_cache=(not self._initialized))
new_value = result.get('fan_mode', None)
self._state = VALUE_TO_SPEED.get(new_value, None)
def attribute_updated(self, attribute, value):
"""Handle attribute update from device."""
attr_name = self.cluster.attributes.get(attribute, [attribute])[0]
_LOGGER.debug("%s: Attribute report '%s'[%s] = %s",
self.entity_id, self.cluster.name, attr_name, value)
if attribute == self.value_attribute:
self._state = VALUE_TO_SPEED.get(value, self._state)
self.async_schedule_update_ha_state()

View File

@ -9,14 +9,12 @@ import logging
from homeassistant.components import light from homeassistant.components import light
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from .core import helpers from .const import (
from .core.const import ( DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, LISTENER_COLOR,
DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, LISTENER_ON_OFF, LISTENER_LEVEL, SIGNAL_ATTR_UPDATED, SIGNAL_SET_LEVEL
REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW) )
from .entity import ZhaEntity from .entity import ZhaEntity
from .core.listeners import (
OnOffListener, LevelListener
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -58,26 +56,6 @@ async def _async_setup_entities(hass, config_entry, async_add_entities,
"""Set up the ZHA lights.""" """Set up the ZHA lights."""
entities = [] entities = []
for discovery_info in discovery_infos: for discovery_info in discovery_infos:
endpoint = discovery_info['endpoint']
if hasattr(endpoint, 'light_color'):
caps = await helpers.safe_read(
endpoint.light_color, ['color_capabilities'])
discovery_info['color_capabilities'] = caps.get(
'color_capabilities')
if discovery_info['color_capabilities'] is None:
# ZCL Version 4 devices don't support the color_capabilities
# attribute. In this version XY support is mandatory, but we
# need to probe to determine if the device supports color
# temperature.
discovery_info['color_capabilities'] = \
CAPABILITIES_COLOR_XY
result = await helpers.safe_read(
endpoint.light_color, ['color_temperature'])
if (result.get('color_temperature') is not
UNSUPPORTED_ATTRIBUTE):
discovery_info['color_capabilities'] |= \
CAPABILITIES_COLOR_TEMP
zha_light = Light(**discovery_info) zha_light = Light(**discovery_info)
entities.append(zha_light) entities.append(zha_light)
@ -89,34 +67,24 @@ class Light(ZhaEntity, light.Light):
_domain = light.DOMAIN _domain = light.DOMAIN
def __init__(self, **kwargs): def __init__(self, unique_id, zha_device, listeners, **kwargs):
"""Initialize the ZHA light.""" """Initialize the ZHA light."""
super().__init__(**kwargs) super().__init__(unique_id, zha_device, listeners, **kwargs)
self._supported_features = 0 self._supported_features = 0
self._color_temp = None self._color_temp = None
self._hs_color = None self._hs_color = None
self._brightness = None self._brightness = None
from zigpy.zcl.clusters.general import OnOff, LevelControl self._on_off_listener = self.cluster_listeners.get(LISTENER_ON_OFF)
self._in_listeners = { self._level_listener = self.cluster_listeners.get(LISTENER_LEVEL)
OnOff.cluster_id: OnOffListener( self._color_listener = self.cluster_listeners.get(LISTENER_COLOR)
self,
self._in_clusters[OnOff.cluster_id]
),
}
if LevelControl.cluster_id in self._in_clusters: if self._level_listener:
self._supported_features |= light.SUPPORT_BRIGHTNESS self._supported_features |= light.SUPPORT_BRIGHTNESS
self._supported_features |= light.SUPPORT_TRANSITION self._supported_features |= light.SUPPORT_TRANSITION
self._brightness = 0 self._brightness = 0
self._in_listeners.update({
LevelControl.cluster_id: LevelListener( if self._color_listener:
self, color_capabilities = self._color_listener.get_color_capabilities()
self._in_clusters[LevelControl.cluster_id]
)
})
import zigpy.zcl.clusters as zcl_clusters
if zcl_clusters.lighting.Color.cluster_id in self._in_clusters:
color_capabilities = kwargs['color_capabilities']
if color_capabilities & CAPABILITIES_COLOR_TEMP: if color_capabilities & CAPABILITIES_COLOR_TEMP:
self._supported_features |= light.SUPPORT_COLOR_TEMP self._supported_features |= light.SUPPORT_COLOR_TEMP
@ -124,131 +92,28 @@ class Light(ZhaEntity, light.Light):
self._supported_features |= light.SUPPORT_COLOR self._supported_features |= light.SUPPORT_COLOR
self._hs_color = (0, 0) self._hs_color = (0, 0)
@property
def zcl_reporting_config(self) -> dict:
"""Return attribute reporting configuration."""
return {
'on_off': {'on_off': REPORT_CONFIG_IMMEDIATE},
'level': {'current_level': REPORT_CONFIG_ASAP},
'light_color': {
'current_x': REPORT_CONFIG_DEFAULT,
'current_y': REPORT_CONFIG_DEFAULT,
'color_temperature': REPORT_CONFIG_DEFAULT,
}
}
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return true if entity is on.""" """Return true if entity is on."""
if self._state is None: if self._state is None:
return False return False
return bool(self._state) return self._state
def set_state(self, state):
"""Set the state."""
self._state = state
self.async_schedule_update_ha_state()
async def async_turn_on(self, **kwargs):
"""Turn the entity on."""
from zigpy.exceptions import DeliveryError
duration = kwargs.get(light.ATTR_TRANSITION, DEFAULT_DURATION)
duration = duration * 10 # tenths of s
if light.ATTR_COLOR_TEMP in kwargs and \
self.supported_features & light.SUPPORT_COLOR_TEMP:
temperature = kwargs[light.ATTR_COLOR_TEMP]
try:
res = await self._endpoint.light_color.move_to_color_temp(
temperature, duration)
_LOGGER.debug("%s: moved to %i color temp: %s",
self.entity_id, temperature, res)
except DeliveryError as ex:
_LOGGER.error("%s: Couldn't change color temp: %s",
self.entity_id, ex)
return
self._color_temp = temperature
if light.ATTR_HS_COLOR in kwargs and \
self.supported_features & light.SUPPORT_COLOR:
self._hs_color = kwargs[light.ATTR_HS_COLOR]
xy_color = color_util.color_hs_to_xy(*self._hs_color)
try:
res = await self._endpoint.light_color.move_to_color(
int(xy_color[0] * 65535),
int(xy_color[1] * 65535),
duration,
)
_LOGGER.debug("%s: moved XY color to (%1.2f, %1.2f): %s",
self.entity_id, xy_color[0], xy_color[1], res)
except DeliveryError as ex:
_LOGGER.error("%s: Couldn't change color temp: %s",
self.entity_id, ex)
return
if self._brightness is not None:
brightness = kwargs.get(
light.ATTR_BRIGHTNESS, self._brightness or 255)
# Move to level with on/off:
try:
res = await self._endpoint.level.move_to_level_with_on_off(
brightness,
duration
)
_LOGGER.debug("%s: moved to %i level with on/off: %s",
self.entity_id, brightness, res)
except DeliveryError as ex:
_LOGGER.error("%s: Couldn't change brightness level: %s",
self.entity_id, ex)
return
self._state = 1
self._brightness = brightness
self.async_schedule_update_ha_state()
return
try:
res = await self._endpoint.on_off.on()
_LOGGER.debug("%s was turned on: %s", self.entity_id, res)
except DeliveryError as ex:
_LOGGER.error("%s: Unable to turn the light on: %s",
self.entity_id, ex)
return
self._state = 1
self.async_schedule_update_ha_state()
async def async_turn_off(self, **kwargs):
"""Turn the entity off."""
from zigpy.exceptions import DeliveryError
duration = kwargs.get(light.ATTR_TRANSITION)
try:
supports_level = self.supported_features & light.SUPPORT_BRIGHTNESS
if duration and supports_level:
res = await self._endpoint.level.move_to_level_with_on_off(
0, duration*10
)
else:
res = await self._endpoint.on_off.off()
_LOGGER.debug("%s was turned off: %s", self.entity_id, res)
except DeliveryError as ex:
_LOGGER.error("%s: Unable to turn the light off: %s",
self.entity_id, ex)
return
self._state = 0
self.async_schedule_update_ha_state()
@property @property
def brightness(self): def brightness(self):
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light."""
return self._brightness return self._brightness
@property
def device_state_attributes(self):
"""Return state attributes."""
return self.state_attributes
def set_level(self, value): def set_level(self, value):
"""Set the brightness of this light between 0..255.""" """Set the brightness of this light between 0..255."""
if value < 0 or value > 255: value = max(0, min(255, value))
return
self._brightness = value self._brightness = value
self.async_schedule_update_ha_state() self.async_set_state(value)
@property @property
def hs_color(self): def hs_color(self):
@ -265,40 +130,82 @@ class Light(ZhaEntity, light.Light):
"""Flag supported features.""" """Flag supported features."""
return self._supported_features return self._supported_features
async def async_update(self): def async_set_state(self, state):
"""Retrieve latest state.""" """Set the state."""
result = await helpers.safe_read(self._endpoint.on_off, ['on_off'], self._state = bool(state)
allow_cache=False, self.async_schedule_update_ha_state()
only_cache=(not self._initialized))
self._state = result.get('on_off', self._state)
if self._supported_features & light.SUPPORT_BRIGHTNESS: async def async_added_to_hass(self):
result = await helpers.safe_read(self._endpoint.level, """Run when about to be added to hass."""
['current_level'], await super().async_added_to_hass()
allow_cache=False, await self.async_accept_signal(
only_cache=( self._on_off_listener, SIGNAL_ATTR_UPDATED, self.async_set_state)
not self._initialized if self._level_listener:
)) await self.async_accept_signal(
self._brightness = result.get('current_level', self._brightness) self._level_listener, SIGNAL_SET_LEVEL, self.set_level)
if self._supported_features & light.SUPPORT_COLOR_TEMP: async def async_turn_on(self, **kwargs):
result = await helpers.safe_read(self._endpoint.light_color, """Turn the entity on."""
['color_temperature'], duration = kwargs.get(light.ATTR_TRANSITION, DEFAULT_DURATION)
allow_cache=False, duration = duration * 10 # tenths of s
only_cache=(
not self._initialized
))
self._color_temp = result.get('color_temperature',
self._color_temp)
if self._supported_features & light.SUPPORT_COLOR: if light.ATTR_COLOR_TEMP in kwargs and \
result = await helpers.safe_read(self._endpoint.light_color, self.supported_features & light.SUPPORT_COLOR_TEMP:
['current_x', 'current_y'], temperature = kwargs[light.ATTR_COLOR_TEMP]
allow_cache=False, success = await self._color_listener.move_to_color_temp(
only_cache=( temperature, duration)
not self._initialized if not success:
)) return
if 'current_x' in result and 'current_y' in result: self._color_temp = temperature
xy_color = (round(result['current_x']/65535, 3),
round(result['current_y']/65535, 3)) if light.ATTR_HS_COLOR in kwargs and \
self._hs_color = color_util.color_xy_to_hs(*xy_color) self.supported_features & light.SUPPORT_COLOR:
hs_color = kwargs[light.ATTR_HS_COLOR]
xy_color = color_util.color_hs_to_xy(*hs_color)
success = await self._color_listener.move_to_color(
int(xy_color[0] * 65535),
int(xy_color[1] * 65535),
duration,
)
if not success:
return
self._hs_color = hs_color
if self._brightness is not None:
brightness = kwargs.get(
light.ATTR_BRIGHTNESS, self._brightness or 255)
success = await self._level_listener.move_to_level_with_on_off(
brightness,
duration
)
if not success:
return
self._state = True
self._brightness = brightness
self.async_schedule_update_ha_state()
return
success = await self._on_off_listener.on()
if not success:
return
self._state = True
self.async_schedule_update_ha_state()
async def async_turn_off(self, **kwargs):
"""Turn the entity off."""
duration = kwargs.get(light.ATTR_TRANSITION)
supports_level = self.supported_features & light.SUPPORT_BRIGHTNESS
success = None
if duration and supports_level:
success = await self._level_listener.move_to_level_with_on_off(
0,
duration*10
)
else:
success = await self._on_off_listener.off()
_LOGGER.debug("%s was turned off: %s", self.entity_id, success)
if not success:
return
self._state = False
self.async_schedule_update_ha_state()

View File

@ -9,11 +9,11 @@ import logging
from homeassistant.components.sensor import DOMAIN from homeassistant.components.sensor import DOMAIN
from homeassistant.const import TEMP_CELSIUS from homeassistant.const import TEMP_CELSIUS
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.temperature import convert as convert_temperature
from .core import helpers
from .core.const import ( from .core.const import (
DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_MAX_INT, DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, HUMIDITY, TEMPERATURE,
REPORT_CONFIG_MIN_INT, REPORT_CONFIG_RPT_CHANGE, ZHA_DISCOVERY_NEW) ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT,
POWER_CONFIGURATION, GENERIC, SENSOR_TYPE, LISTENER_ATTRIBUTE,
LISTENER_ACTIVE_POWER, SIGNAL_ATTR_UPDATED, SIGNAL_STATE_ATTR)
from .entity import ZhaEntity from .entity import ZhaEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -21,6 +21,73 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['zha'] DEPENDENCIES = ['zha']
# Formatter functions
def pass_through_formatter(value):
"""No op update function."""
return value
def temperature_formatter(value):
"""Convert temperature data."""
if value is None:
return None
return round(value / 100, 1)
def humidity_formatter(value):
"""Return the state of the entity."""
if value is None:
return None
return round(float(value) / 100, 1)
def active_power_formatter(value):
"""Return the state of the entity."""
if value is None:
return None
return round(float(value) / 10, 1)
def pressure_formatter(value):
"""Return the state of the entity."""
if value is None:
return None
return round(float(value))
FORMATTER_FUNC_REGISTRY = {
HUMIDITY: humidity_formatter,
TEMPERATURE: temperature_formatter,
PRESSURE: pressure_formatter,
ELECTRICAL_MEASUREMENT: active_power_formatter,
GENERIC: pass_through_formatter,
}
UNIT_REGISTRY = {
HUMIDITY: '%',
TEMPERATURE: TEMP_CELSIUS,
PRESSURE: 'hPa',
ILLUMINANCE: 'lx',
METERING: 'W',
ELECTRICAL_MEASUREMENT: 'W',
POWER_CONFIGURATION: '%',
GENERIC: None
}
LISTENER_REGISTRY = {
ELECTRICAL_MEASUREMENT: LISTENER_ACTIVE_POWER,
}
POLLING_REGISTRY = {
ELECTRICAL_MEASUREMENT: True
}
FORCE_UPDATE_REGISTRY = {
ELECTRICAL_MEASUREMENT: True
}
async def async_setup_platform(hass, config, async_add_entities, async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None): discovery_info=None):
"""Old way of setting up Zigbee Home Automation sensors.""" """Old way of setting up Zigbee Home Automation sensors."""
@ -56,279 +123,59 @@ async def _async_setup_entities(hass, config_entry, async_add_entities,
async def make_sensor(discovery_info): async def make_sensor(discovery_info):
"""Create ZHA sensors factory.""" """Create ZHA sensors factory."""
from zigpy.zcl.clusters.measurement import ( return Sensor(**discovery_info)
RelativeHumidity, TemperatureMeasurement, PressureMeasurement,
IlluminanceMeasurement
)
from zigpy.zcl.clusters.smartenergy import Metering
from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement
from zigpy.zcl.clusters.general import PowerConfiguration
in_clusters = discovery_info['in_clusters']
if 'sub_component' in discovery_info:
sensor = discovery_info['sub_component'](**discovery_info)
elif RelativeHumidity.cluster_id in in_clusters:
sensor = RelativeHumiditySensor(**discovery_info)
elif PowerConfiguration.cluster_id in in_clusters:
sensor = GenericBatterySensor(**discovery_info)
elif TemperatureMeasurement.cluster_id in in_clusters:
sensor = TemperatureSensor(**discovery_info)
elif PressureMeasurement.cluster_id in in_clusters:
sensor = PressureSensor(**discovery_info)
elif IlluminanceMeasurement.cluster_id in in_clusters:
sensor = IlluminanceMeasurementSensor(**discovery_info)
elif Metering.cluster_id in in_clusters:
sensor = MeteringSensor(**discovery_info)
elif ElectricalMeasurement.cluster_id in in_clusters:
sensor = ElectricalMeasurementSensor(**discovery_info)
return sensor
else:
sensor = Sensor(**discovery_info)
return sensor
class Sensor(ZhaEntity): class Sensor(ZhaEntity):
"""Base ZHA sensor.""" """Base ZHA sensor."""
_domain = DOMAIN _domain = DOMAIN
value_attribute = 0
min_report_interval = REPORT_CONFIG_MIN_INT
max_report_interval = REPORT_CONFIG_MAX_INT
min_reportable_change = REPORT_CONFIG_RPT_CHANGE
report_config = (min_report_interval, max_report_interval,
min_reportable_change)
def __init__(self, **kwargs): def __init__(self, unique_id, zha_device, listeners, **kwargs):
"""Init ZHA Sensor instance.""" """Init this sensor."""
super().__init__(**kwargs) super().__init__(unique_id, zha_device, listeners, **kwargs)
self._cluster = list(kwargs['in_clusters'].values())[0] sensor_type = kwargs.get(SENSOR_TYPE, GENERIC)
self._unit = UNIT_REGISTRY.get(sensor_type)
self._formatter_function = FORMATTER_FUNC_REGISTRY.get(
sensor_type,
pass_through_formatter
)
self._force_update = FORCE_UPDATE_REGISTRY.get(
sensor_type,
False
)
self._should_poll = POLLING_REGISTRY.get(
sensor_type,
False
)
self._listener = self.cluster_listeners.get(
LISTENER_REGISTRY.get(sensor_type, LISTENER_ATTRIBUTE)
)
async def async_added_to_hass(self):
"""Run when about to be added to hass."""
await super().async_added_to_hass()
await self.async_accept_signal(
self._listener, SIGNAL_ATTR_UPDATED, self.async_set_state)
await self.async_accept_signal(
self._listener, SIGNAL_STATE_ATTR,
self.async_update_state_attribute)
@property @property
def zcl_reporting_config(self) -> dict: def unit_of_measurement(self):
"""Return a dict of attribute reporting configuration.""" """Return the unit of measurement of this entity."""
return { return self._unit
self.cluster: {self.value_attribute: self.report_config}
}
@property
def cluster(self):
"""Return Sensor's cluster."""
return self._cluster
@property @property
def state(self) -> str: def state(self) -> str:
"""Return the state of the entity.""" """Return the state of the entity."""
if self._state is None:
return None
if isinstance(self._state, float): if isinstance(self._state, float):
return str(round(self._state, 2)) return str(round(self._state, 2))
return self._state return self._state
def attribute_updated(self, attribute, value): def async_set_state(self, state):
"""Handle attribute update from device.""" """Handle state update from listener."""
_LOGGER.debug("Attribute updated: %s %s %s", self, attribute, value) self._state = self._formatter_function(state)
if attribute == self.value_attribute: self.async_schedule_update_ha_state()
self._state = value
self.async_schedule_update_ha_state()
async def async_update(self):
"""Retrieve latest state."""
result = await helpers.safe_read(
self.cluster,
[self.value_attribute],
allow_cache=False,
only_cache=(not self._initialized)
)
self._state = result.get(self.value_attribute, self._state)
class GenericBatterySensor(Sensor):
"""ZHA generic battery sensor."""
report_attribute = 32
value_attribute = 33
battery_sizes = {
0: 'No battery',
1: 'Built in',
2: 'Other',
3: 'AA',
4: 'AAA',
5: 'C',
6: 'D',
7: 'CR2',
8: 'CR123A',
9: 'CR2450',
10: 'CR2032',
11: 'CR1632',
255: 'Unknown'
}
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return '%'
@property
def zcl_reporting_config(self) -> dict:
"""Return a dict of attribute reporting configuration."""
return {
self.cluster: {
self.value_attribute: self.report_config,
self.report_attribute: self.report_config
}
}
async def async_update(self):
"""Retrieve latest state."""
_LOGGER.debug("%s async_update", self.entity_id)
result = await helpers.safe_read(
self._endpoint.power,
[
'battery_size',
'battery_quantity',
'battery_percentage_remaining'
],
allow_cache=False,
only_cache=(not self._initialized)
)
self._device_state_attributes['battery_size'] = self.battery_sizes.get(
result.get('battery_size', 255), 'Unknown')
self._device_state_attributes['battery_quantity'] = result.get(
'battery_quantity', 'Unknown')
self._state = result.get('battery_percentage_remaining', self._state)
@property
def state(self):
"""Return the state of the entity."""
if self._state == 'unknown' or self._state is None:
return None
return self._state
class TemperatureSensor(Sensor):
"""ZHA temperature sensor."""
min_reportable_change = 50 # 0.5'C
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return self.hass.config.units.temperature_unit
@property
def state(self):
"""Return the state of the entity."""
if self._state is None:
return None
celsius = self._state / 100
return round(convert_temperature(celsius,
TEMP_CELSIUS,
self.unit_of_measurement),
1)
class RelativeHumiditySensor(Sensor):
"""ZHA relative humidity sensor."""
min_reportable_change = 50 # 0.5%
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return '%'
@property
def state(self):
"""Return the state of the entity."""
if self._state is None:
return None
return round(float(self._state) / 100, 1)
class PressureSensor(Sensor):
"""ZHA pressure sensor."""
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return 'hPa'
@property
def state(self):
"""Return the state of the entity."""
if self._state is None:
return None
return round(float(self._state))
class IlluminanceMeasurementSensor(Sensor):
"""ZHA lux sensor."""
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return 'lx'
@property
def state(self):
"""Return the state of the entity."""
return self._state
class MeteringSensor(Sensor):
"""ZHA Metering sensor."""
value_attribute = 1024
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return 'W'
@property
def state(self):
"""Return the state of the entity."""
if self._state is None:
return None
return round(float(self._state))
class ElectricalMeasurementSensor(Sensor):
"""ZHA Electrical Measurement sensor."""
value_attribute = 1291
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return 'W'
@property
def force_update(self) -> bool:
"""Force update this entity."""
return True
@property
def state(self):
"""Return the state of the entity."""
if self._state is None:
return None
return round(float(self._state) / 10, 1)
@property
def should_poll(self) -> bool:
"""Poll state from device."""
return True
async def async_update(self):
"""Retrieve latest state."""
_LOGGER.debug("%s async_update", self.entity_id)
result = await helpers.safe_read(
self.cluster, ['active_power'],
allow_cache=False, only_cache=(not self._initialized))
self._state = result.get('active_power', self._state)

View File

@ -8,9 +8,10 @@ import logging
from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.components.switch import DOMAIN, SwitchDevice
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .core import helpers
from .core.const import ( from .core.const import (
DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW) DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, LISTENER_ON_OFF,
SIGNAL_ATTR_UPDATED
)
from .entity import ZhaEntity from .entity import ZhaEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -55,69 +56,39 @@ class Switch(ZhaEntity, SwitchDevice):
"""ZHA switch.""" """ZHA switch."""
_domain = DOMAIN _domain = DOMAIN
value_attribute = 0
def attribute_updated(self, attribute, value): def __init__(self, **kwargs):
"""Handle attribute update from device.""" """Initialize the ZHA switch."""
cluster = self._endpoint.on_off super().__init__(**kwargs)
attr_name = cluster.attributes.get(attribute, [attribute])[0] self._on_off_listener = self.cluster_listeners.get(LISTENER_ON_OFF)
_LOGGER.debug("%s: Attribute '%s' on cluster '%s' updated to %s",
self.entity_id, attr_name, cluster.ep_attribute, value)
if attribute == self.value_attribute:
self._state = value
self.async_schedule_update_ha_state()
@property
def zcl_reporting_config(self) -> dict:
"""Retrun a dict of attribute reporting configuration."""
return {
self.cluster: {'on_off': REPORT_CONFIG_IMMEDIATE}
}
@property
def cluster(self):
"""Entity's cluster."""
return self._endpoint.on_off
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return if the switch is on based on the statemachine.""" """Return if the switch is on based on the statemachine."""
if self._state is None: if self._state is None:
return False return False
return bool(self._state) return self._state
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn the entity on.""" """Turn the entity on."""
from zigpy.exceptions import DeliveryError await self._on_off_listener.on()
try:
res = await self._endpoint.on_off.on()
_LOGGER.debug("%s: turned 'on': %s", self.entity_id, res[1])
except DeliveryError as ex:
_LOGGER.error("%s: Unable to turn the switch on: %s",
self.entity_id, ex)
return
self._state = 1
self.async_schedule_update_ha_state()
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs):
"""Turn the entity off.""" """Turn the entity off."""
from zigpy.exceptions import DeliveryError await self._on_off_listener.off()
try:
res = await self._endpoint.on_off.off()
_LOGGER.debug("%s: turned 'off': %s", self.entity_id, res[1])
except DeliveryError as ex:
_LOGGER.error("%s: Unable to turn the switch off: %s",
self.entity_id, ex)
return
self._state = 0 def async_set_state(self, state):
"""Handle state update from listener."""
self._state = bool(state)
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
async def async_update(self): @property
"""Retrieve latest state.""" def device_state_attributes(self):
result = await helpers.safe_read(self.cluster, """Return state attributes."""
['on_off'], return self.state_attributes
allow_cache=False,
only_cache=(not self._initialized)) async def async_added_to_hass(self):
self._state = result.get('on_off', self._state) """Run when about to be added to hass."""
await super().async_added_to_hass()
await self.async_accept_signal(
self._on_off_listener, SIGNAL_ATTR_UPDATED, self.async_set_state)

View File

@ -3,9 +3,12 @@ from unittest.mock import patch
import pytest import pytest
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.zha.core.const import ( from homeassistant.components.zha.core.const import (
DOMAIN, DATA_ZHA DOMAIN, DATA_ZHA, COMPONENTS
) )
from homeassistant.components.zha.core.gateway import ZHAGateway from homeassistant.components.zha.core.gateway import ZHAGateway
from homeassistant.components.zha.core.gateway import establish_device_mappings
from homeassistant.components.zha.core.listeners \
import populate_listener_registry
from .common import async_setup_entry from .common import async_setup_entry
@ -25,6 +28,12 @@ def zha_gateway_fixture(hass):
Create a ZHAGateway object that can be used to interact with as if we Create a ZHAGateway object that can be used to interact with as if we
had a real zigbee network running. had a real zigbee network running.
""" """
populate_listener_registry()
establish_device_mappings()
for component in COMPONENTS:
hass.data[DATA_ZHA][component] = (
hass.data[DATA_ZHA].get(component, {})
)
return ZHAGateway(hass, {}) return ZHAGateway(hass, {})

View File

@ -48,6 +48,7 @@ async def test_binary_sensor(hass, config_entry, zha_gateway):
# load up binary_sensor domain # load up binary_sensor domain
await hass.config_entries.async_forward_entry_setup( await hass.config_entries.async_forward_entry_setup(
config_entry, DOMAIN) config_entry, DOMAIN)
await zha_gateway.accept_zigbee_messages({})
await hass.async_block_till_done() await hass.async_block_till_done()
# on off binary_sensor # on off binary_sensor

View File

@ -26,6 +26,7 @@ async def test_fan(hass, config_entry, zha_gateway):
# load up fan domain # load up fan domain
await hass.config_entries.async_forward_entry_setup( await hass.config_entries.async_forward_entry_setup(
config_entry, DOMAIN) config_entry, DOMAIN)
await zha_gateway.accept_zigbee_messages({})
await hass.async_block_till_done() await hass.async_block_till_done()
cluster = zigpy_device.endpoints.get(1).fan cluster = zigpy_device.endpoints.get(1).fan

View File

@ -40,6 +40,7 @@ async def test_light(hass, config_entry, zha_gateway):
# load up light domain # load up light domain
await hass.config_entries.async_forward_entry_setup( await hass.config_entries.async_forward_entry_setup(
config_entry, DOMAIN) config_entry, DOMAIN)
await zha_gateway.accept_zigbee_messages({})
await hass.async_block_till_done() await hass.async_block_till_done()
# on off light # on off light

View File

@ -92,6 +92,7 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids):
# load up sensor domain # load up sensor domain
await hass.config_entries.async_forward_entry_setup( await hass.config_entries.async_forward_entry_setup(
config_entry, DOMAIN) config_entry, DOMAIN)
await zha_gateway.accept_zigbee_messages({})
await hass.async_block_till_done() await hass.async_block_till_done()
# put the other relevant info in the device info dict # put the other relevant info in the device info dict

View File

@ -24,6 +24,7 @@ async def test_switch(hass, config_entry, zha_gateway):
# load up switch domain # load up switch domain
await hass.config_entries.async_forward_entry_setup( await hass.config_entries.async_forward_entry_setup(
config_entry, DOMAIN) config_entry, DOMAIN)
await zha_gateway.accept_zigbee_messages({})
await hass.async_block_till_done() await hass.async_block_till_done()
cluster = zigpy_device.endpoints.get(1).on_off cluster = zigpy_device.endpoints.get(1).on_off
@ -44,6 +45,7 @@ async def test_switch(hass, config_entry, zha_gateway):
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF assert hass.states.get(entity_id).state == STATE_OFF
# turn on from HA
with patch( with patch(
'zigpy.zcl.Cluster.request', 'zigpy.zcl.Cluster.request',
return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])):
@ -55,6 +57,7 @@ async def test_switch(hass, config_entry, zha_gateway):
assert cluster.request.call_args == call( assert cluster.request.call_args == call(
False, ON, (), expect_reply=True, manufacturer=None) False, ON, (), expect_reply=True, manufacturer=None)
# turn off from HA
with patch( with patch(
'zigpy.zcl.Cluster.request', 'zigpy.zcl.Cluster.request',
return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])):
@ -66,5 +69,6 @@ async def test_switch(hass, config_entry, zha_gateway):
assert cluster.request.call_args == call( assert cluster.request.call_args == call(
False, OFF, (), expect_reply=True, manufacturer=None) False, OFF, (), expect_reply=True, manufacturer=None)
# test joining a new switch to the network and HA
await async_test_device_join( await async_test_device_join(
hass, zha_gateway, OnOff.cluster_id, DOMAIN, expected_state=STATE_OFF) hass, zha_gateway, OnOff.cluster_id, DOMAIN, expected_state=STATE_OFF)