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/zha/__init__.py
homeassistant/components/zha/api.py
homeassistant/components/zha/binary_sensor.py
homeassistant/components/zha/const.py
homeassistant/components/zha/core/const.py
homeassistant/components/zha/core/device.py
@ -667,11 +666,8 @@ omit =
homeassistant/components/zha/core/listeners.py
homeassistant/components/zha/device_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/sensor.py
homeassistant/components/zha/switch.py
homeassistant/components/zigbee/*
homeassistant/components/zoneminder/*
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
https://home-assistant.io/components/zha/
"""
import asyncio
import logging
import os
import types
@ -17,14 +18,15 @@ from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
# Loading the config flow file will register the flow
from . import config_flow # noqa # pylint: disable=unused-import
from . import api
from .core.gateway import ZHAGateway
from .const import (
from .core import ZHAGateway
from .core.const import (
COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG,
CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, DATA_ZHA_BRIDGE_ID,
DATA_ZHA_CONFIG, DATA_ZHA_CORE_COMPONENT, DATA_ZHA_DISPATCHERS,
DATA_ZHA_RADIO, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME,
DEFAULT_RADIO_TYPE, DOMAIN, RadioType, DATA_ZHA_CORE_EVENTS,
ENABLE_QUIRKS)
DEFAULT_RADIO_TYPE, DOMAIN, RadioType, DATA_ZHA_CORE_EVENTS, ENABLE_QUIRKS)
from .core.gateway import establish_device_mappings
from .core.listeners import populate_listener_registry
REQUIREMENTS = [
'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.
"""
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][DATA_ZHA_DISPATCHERS] = []
config = hass.data[DATA_ZHA].get(DATA_ZHA_CONFIG, {})
if config.get(ENABLE_QUIRKS, True):
@ -137,14 +146,32 @@ async def async_setup_entry(hass, config_entry):
ClusterPersistingListener
)
application_controller = ControllerApplication(radio, database)
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)
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():
hass.async_create_task(
zha_gateway.async_device_initialized(device, False))
init_tasks.append(zha_gateway.async_device_initialized(device, False))
await asyncio.gather(*init_tasks)
device_registry = await \
hass.helpers.device_registry.async_get_registry()
@ -157,8 +184,6 @@ async def async_setup_entry(hass, config_entry):
model=radio_description,
)
hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(application_controller.ieee)
for component in COMPONENTS:
hass.async_create_task(
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.const import ATTR_ENTITY_ID
import homeassistant.helpers.config_validation as cv
from .device_entity import ZhaDeviceEntity
from .const import (
from .core.const import (
DOMAIN, ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, ATTR_ATTRIBUTE, ATTR_VALUE,
ATTR_MANUFACTURER, ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ARGS, IN, OUT,
CLIENT_COMMANDS, SERVER_COMMANDS, SERVER)
@ -118,115 +117,7 @@ SCHEMA_WS_CLUSTER_COMMANDS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
})
@websocket_api.async_response
async def websocket_entity_cluster_attributes(hass, connection, msg):
"""Return a list of cluster attributes."""
entity_id = msg[ATTR_ENTITY_ID]
cluster_id = msg[ATTR_CLUSTER_ID]
cluster_type = msg[ATTR_CLUSTER_TYPE]
component = hass.data.get(entity_id.split('.')[0])
entity = component.get_entity(entity_id)
cluster_attributes = []
if entity is not None:
res = await entity.get_cluster_attributes(cluster_id, cluster_type)
if res is not None:
for attr_id in res:
cluster_attributes.append(
{
ID: attr_id,
NAME: res[attr_id][0]
}
)
_LOGGER.debug("Requested attributes for: %s %s %s %s",
"{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id),
"{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type),
"{}: [{}]".format(ATTR_ENTITY_ID, entity_id),
"{}: [{}]".format(RESPONSE, cluster_attributes)
)
connection.send_message(websocket_api.result_message(
msg[ID],
cluster_attributes
))
@websocket_api.async_response
async def websocket_entity_cluster_commands(hass, connection, msg):
"""Return a list of cluster commands."""
entity_id = msg[ATTR_ENTITY_ID]
cluster_id = msg[ATTR_CLUSTER_ID]
cluster_type = msg[ATTR_CLUSTER_TYPE]
component = hass.data.get(entity_id.split('.')[0])
entity = component.get_entity(entity_id)
cluster_commands = []
if entity is not None:
res = await entity.get_cluster_commands(cluster_id, cluster_type)
if res is not None:
for cmd_id in res[CLIENT_COMMANDS]:
cluster_commands.append(
{
TYPE: CLIENT,
ID: cmd_id,
NAME: res[CLIENT_COMMANDS][cmd_id][0]
}
)
for cmd_id in res[SERVER_COMMANDS]:
cluster_commands.append(
{
TYPE: SERVER,
ID: cmd_id,
NAME: res[SERVER_COMMANDS][cmd_id][0]
}
)
_LOGGER.debug("Requested commands for: %s %s %s %s",
"{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id),
"{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type),
"{}: [{}]".format(ATTR_ENTITY_ID, entity_id),
"{}: [{}]".format(RESPONSE, cluster_commands)
)
connection.send_message(websocket_api.result_message(
msg[ID],
cluster_commands
))
@websocket_api.async_response
async def websocket_read_zigbee_cluster_attributes(hass, connection, msg):
"""Read zigbee attribute for cluster on zha entity."""
entity_id = msg[ATTR_ENTITY_ID]
cluster_id = msg[ATTR_CLUSTER_ID]
cluster_type = msg[ATTR_CLUSTER_TYPE]
attribute = msg[ATTR_ATTRIBUTE]
component = hass.data.get(entity_id.split('.')[0])
entity = component.get_entity(entity_id)
clusters = await entity.get_clusters()
cluster = clusters[cluster_type][cluster_id]
manufacturer = msg.get(ATTR_MANUFACTURER) or None
success = failure = None
if entity is not None:
success, failure = await cluster.read_attributes(
[attribute],
allow_cache=False,
only_cache=False,
manufacturer=manufacturer
)
_LOGGER.debug("Read attribute for: %s %s %s %s %s %s %s",
"{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id),
"{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type),
"{}: [{}]".format(ATTR_ENTITY_ID, entity_id),
"{}: [{}]".format(ATTR_ATTRIBUTE, attribute),
"{}: [{}]".format(ATTR_MANUFACTURER, manufacturer),
"{}: [{}]".format(RESPONSE, str(success.get(attribute))),
"{}: [{}]".format('failure', failure)
)
connection.send_message(websocket_api.result_message(
msg[ID],
str(success.get(attribute))
))
def async_load_api(hass, application_controller, listener):
def async_load_api(hass, application_controller, zha_gateway):
"""Set up the web socket API."""
async def permit(service):
"""Allow devices to join this network."""
@ -256,11 +147,12 @@ def async_load_api(hass, application_controller, listener):
attribute = service.data.get(ATTR_ATTRIBUTE)
value = service.data.get(ATTR_VALUE)
manufacturer = service.data.get(ATTR_MANUFACTURER) or None
component = hass.data.get(entity_id.split('.')[0])
entity = component.get_entity(entity_id)
entity_ref = zha_gateway.get_entity_reference(entity_id)
response = None
if entity is not None:
response = await entity.write_zigbe_attribute(
if entity_ref is not None:
response = await entity_ref.zha_device.write_zigbee_attribute(
list(entity_ref.cluster_listeners.values())[
0].cluster.endpoint.endpoint_id,
cluster_id,
attribute,
value,
@ -292,11 +184,13 @@ def async_load_api(hass, application_controller, listener):
command_type = service.data.get(ATTR_COMMAND_TYPE)
args = service.data.get(ATTR_ARGS)
manufacturer = service.data.get(ATTR_MANUFACTURER) or None
component = hass.data.get(entity_id.split('.')[0])
entity = component.get_entity(entity_id)
entity_ref = zha_gateway.get_entity_reference(entity_id)
zha_device = entity_ref.zha_device
response = None
if entity is not None:
response = await entity.issue_cluster_command(
if entity_ref is not None:
response = await zha_device.issue_cluster_command(
list(entity_ref.cluster_listeners.values())[
0].cluster.endpoint.endpoint_id,
cluster_id,
command,
command_type,
@ -325,11 +219,9 @@ def async_load_api(hass, application_controller, listener):
async def websocket_reconfigure_node(hass, connection, msg):
"""Reconfigure a ZHA nodes entities by its ieee address."""
ieee = msg[ATTR_IEEE]
entities = listener.get_entities_for_ieee(ieee)
device = zha_gateway.get_device(ieee)
_LOGGER.debug("Reconfiguring node with ieee_address: %s", ieee)
for entity in entities:
if hasattr(entity, 'async_configure'):
hass.async_create_task(entity.async_configure())
hass.async_create_task(device.async_configure())
hass.components.websocket_api.async_register_command(
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):
"""Return a dict of all zha entities grouped 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)
entities_by_ieee[ieee_string] = []
for entity in entities:
if not isinstance(entity, ZhaDeviceEntity):
entities_by_ieee[ieee_string].append({
ATTR_ENTITY_ID: entity.entity_id,
DEVICE_INFO: entity.device_info
})
entities_by_ieee[ieee_string].append({
ATTR_ENTITY_ID: entity.reference_id,
DEVICE_INFO: entity.device_info
})
connection.send_message(websocket_api.result_message(
msg[ID],
entities_by_ieee
@ -363,24 +255,25 @@ def async_load_api(hass, application_controller, listener):
async def websocket_entity_clusters(hass, connection, msg):
"""Return a list of entity clusters."""
entity_id = msg[ATTR_ENTITY_ID]
entities = listener.get_entities_for_ieee(msg[ATTR_IEEE])
entity = next(
ent for ent in entities if ent.entity_id == entity_id)
entity_clusters = await entity.get_clusters()
entity_ref = zha_gateway.get_entity_reference(entity_id)
clusters = []
for cluster_id, cluster in entity_clusters[IN].items():
clusters.append({
TYPE: IN,
ID: cluster_id,
NAME: cluster.__class__.__name__
})
for cluster_id, cluster in entity_clusters[OUT].items():
clusters.append({
TYPE: OUT,
ID: cluster_id,
NAME: cluster.__class__.__name__
})
if entity_ref is not None:
for listener in entity_ref.cluster_listeners.values():
cluster = listener.cluster
in_clusters = cluster.endpoint.in_clusters.values()
out_clusters = cluster.endpoint.out_clusters.values()
if cluster in in_clusters:
clusters.append({
TYPE: IN,
ID: cluster.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(
msg[ID],
@ -392,16 +285,141 @@ def async_load_api(hass, application_controller, listener):
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(
WS_ENTITY_CLUSTER_ATTRIBUTES, websocket_entity_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(
WS_ENTITY_CLUSTER_COMMANDS, websocket_entity_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(
WS_READ_CLUSTER_ATTRIBUTE, websocket_read_zigbee_cluster_attributes,
SCHEMA_WS_READ_CLUSTER_ATTRIBUTE

View File

@ -7,16 +7,13 @@ at https://home-assistant.io/components/binary_sensor.zha/
import logging
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.restore_state import RestoreEntity
from .core import helpers
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 .core.listeners import (
OnOffListener, LevelListener
)
_LOGGER = logging.getLogger(__name__)
@ -31,7 +28,20 @@ CLASS_MAPPING = {
0x002b: 'gas',
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,
@ -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,
discovery_infos):
"""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 = []
for discovery_info in discovery_infos:
if IasZone.cluster_id in discovery_info['in_clusters']:
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))
entities.append(BinarySensor(**discovery_info))
async_add_entities(entities, update_before_add=True)
async def _async_setup_iaszone(discovery_info):
device_class = None
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."""
class BinarySensor(ZhaEntity, BinarySensorDevice):
"""ZHA BinarySensor."""
_domain = DOMAIN
_device_class = None
value_attribute = 0
def __init__(self, device_class, **kwargs):
def __init__(self, **kwargs):
"""Initialize the ZHA binary sensor."""
super().__init__(**kwargs)
self._device_class = device_class
self._cluster = list(kwargs['in_clusters'].values())[0]
self._device_state_attributes = {}
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):
"""Handle attribute update from device."""
_LOGGER.debug("Attribute updated: %s %s %s", self, attribute, value)
if attribute == self.value_attribute:
self._state = bool(value)
self.async_schedule_update_ha_state()
async def _determine_device_class(self):
"""Determine the device class for this binary sensor."""
device_class_supplier = DEVICE_CLASS_REGISTRY.get(
self._zha_sensor_type)
if callable(device_class_supplier):
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):
"""Run when about to be added to hass."""
self._device_class = await self._determine_device_class()
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)
self._state = old_state.state == STATE_ON
@property
def cluster(self):
"""Zigbee cluster for this entity."""
return self._cluster
@property
def zcl_reporting_config(self):
"""ZHA reporting configuration."""
return {self.cluster: {self.value_attribute: REPORT_CONFIG_IMMEDIATE}}
if self._level_listener:
await self.async_accept_signal(
self._level_listener, SIGNAL_SET_LEVEL, self.set_level)
await self.async_accept_signal(
self._level_listener, SIGNAL_MOVE_LEVEL, self.move_level)
if self._on_off_listener:
await self.async_accept_signal(
self._on_off_listener, SIGNAL_ATTR_UPDATED,
self.async_set_state)
if self._zone_listener:
await self.async_accept_signal(
self._zone_listener, SIGNAL_ATTR_UPDATED, self.async_set_state)
if self._attr_listener:
await self.async_accept_signal(
self._attr_listener, SIGNAL_ATTR_UPDATED, self.async_set_state)
@property
def is_on(self) -> bool:
@ -315,3 +136,32 @@ class BinarySensor(RestoreEntity, ZhaEntity, BinarySensorDevice):
def device_class(self) -> str:
"""Return device class from component DEVICE_CLASSES."""
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
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'
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_EVENT_RELAY = 'event_relay'
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_REMOVE = 'remove'
class RadioType(enum.Enum):
@ -78,9 +106,10 @@ DISCOVERY_KEY = 'zha_discovery_info'
DEVICE_CLASS = {}
SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {}
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {}
CLUSTER_REPORT_CONFIGS = {}
CUSTOM_CLUSTER_MAPPINGS = {}
COMPONENT_CLUSTERS = {}
EVENTABLE_CLUSTERS = []
EVENT_RELAY_CLUSTERS = []
REPORT_CONFIG_MAX_INT = 900
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_CLUSTER_ID, ATTR_ATTRIBUTE, ATTR_VALUE, ATTR_COMMAND, SERVER,
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
@ -30,11 +30,14 @@ class ZHADevice:
self._zigpy_device = zigpy_device
# Get first non ZDO endpoint id to use to get manufacturer and model
endpoint_ids = zigpy_device.endpoints.keys()
ept_id = next(ept_id for ept_id in endpoint_ids if ept_id != 0)
self._manufacturer = zigpy_device.endpoints[ept_id].manufacturer
self._model = zigpy_device.endpoints[ept_id].model
self._manufacturer = UNKNOWN
self._model = UNKNOWN
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._cluster_listeners = {}
self.cluster_listeners = {}
self._relay_listeners = []
self._all_listeners = []
self._name = "{} {}".format(
@ -101,21 +104,11 @@ class ZHADevice:
"""Return the gateway for this device."""
return self._zha_gateway
@property
def cluster_listeners(self):
"""Return cluster listeners for device."""
return self._cluster_listeners.values()
@property
def all_listeners(self):
"""Return cluster listeners and relay listeners for device."""
return self._all_listeners
@property
def cluster_listener_keys(self):
"""Return cluster listeners for device."""
return self._cluster_listeners.keys()
@property
def available_signal(self):
"""Signal to use to subscribe to device availability changes."""
@ -157,17 +150,13 @@ class ZHADevice:
"""Add cluster listener to device."""
# only keep 1 power listener
if cluster_listener.name is LISTENER_BATTERY and \
LISTENER_BATTERY in self._cluster_listeners:
LISTENER_BATTERY in self.cluster_listeners:
return
self._all_listeners.append(cluster_listener)
if isinstance(cluster_listener, EventRelayListener):
self._relay_listeners.append(cluster_listener)
else:
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)
self.cluster_listeners[cluster_listener.name] = cluster_listener
async def async_configure(self):
"""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/
"""
import asyncio
import collections
import itertools
import logging
from homeassistant import const as ha_const
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 .const import (
COMPONENTS, CONF_DEVICE_CONFIG, DATA_ZHA, DATA_ZHA_CORE_COMPONENT, DOMAIN,
ZHA_DISCOVERY_NEW, EVENTABLE_CLUSTERS, DATA_ZHA_CORE_EVENTS, DEVICE_CLASS,
SINGLE_INPUT_CLUSTER_DEVICE_CLASS, SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS,
CUSTOM_CLUSTER_MAPPINGS, COMPONENT_CLUSTERS)
ZHA_DISCOVERY_NEW, DEVICE_CLASS, SINGLE_INPUT_CLUSTER_DEVICE_CLASS,
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, COMPONENT_CLUSTERS, HUMIDITY,
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 ..event import ZhaEvent, ZhaRelayEvent
from .listeners import (
LISTENER_REGISTRY, AttributeListener, EventRelayListener, ZDOListener)
from .helpers import convert_ieee
_LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {}
BINARY_SENSOR_TYPES = {}
EntityReference = collections.namedtuple(
'EntityReference', 'reference_id zha_device cluster_listeners device_info')
class ZHAGateway:
"""Gateway that handles events that happen on the ZHA Zigbee network."""
@ -31,16 +45,9 @@ class ZHAGateway:
self._hass = hass
self._config = config
self._component = EntityComponent(_LOGGER, DOMAIN, hass)
self._devices = {}
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_EVENTS] = self._events
def device_joined(self, device):
"""Handle device joined.
@ -67,197 +74,310 @@ class ZHAGateway:
def device_removed(self, device):
"""Handle device being removed from the network."""
for device_entity in self._device_registry[device.ieee]:
self._hass.async_create_task(device_entity.async_remove())
if device.ieee in self._events:
self._events.pop(device.ieee)
def get_device_entity(self, ieee_str):
"""Return ZHADeviceEntity for given ieee."""
ieee = convert_ieee(ieee_str)
if ieee in self._device_registry:
entities = self._device_registry[ieee]
entity = next(
ent for ent in entities if isinstance(ent, ZhaDeviceEntity))
return entity
return None
def get_entities_for_ieee(self, ieee_str):
"""Return list of entities for given ieee."""
ieee = convert_ieee(ieee_str)
if ieee in self._device_registry:
return self._device_registry[ieee]
return []
@property
def device_registry(self) -> str:
"""Return devices."""
return self._device_registry
async def async_device_initialized(self, device, join):
"""Handle device joined and basic information discovered (async)."""
import zigpy.profiles
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:
device = self._devices.pop(device.ieee, None)
self._device_registry.pop(device.ieee, None)
if device is not None:
self._hass.async_create_task(device.async_unsub_dispatcher())
async_dispatcher_send(
self._hass,
ZHA_DISCOVERY_NEW.format(component),
discovery_info
"{}_{}".format(SIGNAL_REMOVE, str(device.ieee))
)
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():
@ -266,19 +386,16 @@ def establish_device_mappings():
These cannot be module level, as importing bellows must be done in a
in a function.
"""
from zigpy import zcl, quirks
from zigpy import zcl
from zigpy.profiles import PROFILES, zha, zll
from ..sensor import RelativeHumiditySensor
if zha.PROFILE_ID not in DEVICE_CLASS:
DEVICE_CLASS[zha.PROFILE_ID] = {}
if zll.PROFILE_ID not in DEVICE_CLASS:
DEVICE_CLASS[zll.PROFILE_ID] = {}
EVENTABLE_CLUSTERS.append(zcl.clusters.general.AnalogInput.cluster_id)
EVENTABLE_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id)
EVENTABLE_CLUSTERS.append(zcl.clusters.general.MultistateInput.cluster_id)
EVENTABLE_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id)
EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id)
EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id)
DEVICE_CLASS[zha.PROFILE_ID].update({
zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor',
@ -293,6 +410,7 @@ def establish_device_mappings():
zha.DeviceType.DIMMER_SWITCH: 'binary_sensor',
zha.DeviceType.COLOR_DIMMER_SWITCH: 'binary_sensor',
})
DEVICE_CLASS[zll.PROFILE_ID].update({
zll.DeviceType.ON_OFF_LIGHT: 'light',
zll.DeviceType.ON_OFF_PLUGIN_UNIT: 'switch',
@ -321,14 +439,97 @@ def establish_device_mappings():
zcl.clusters.measurement.OccupancySensing: 'binary_sensor',
zcl.clusters.hvac.Fan: 'fan',
})
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({
zcl.clusters.general.OnOff: 'binary_sensor',
})
# A map of device/cluster to component/sub-component
CUSTOM_CLUSTER_MAPPINGS.update({
(quirks.smartthings.SmartthingsTemperatureHumiditySensor, 64581):
('sensor', RelativeHumiditySensor)
SENSOR_TYPES.update({
zcl.clusters.measurement.RelativeHumidity.cluster_id: HUMIDITY,
zcl.clusters.measurement.TemperatureMeasurement.cluster_id:
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

View File

@ -5,20 +5,48 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/
"""
import asyncio
from enum import Enum
from functools import wraps
import logging
from random import uniform
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__)
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."""
cmd = cluster.server_commands.get(command_id, [command_id])[0]
_LOGGER.debug(
"%s: received '%s' command with %s args on cluster_id '%s' tsn '%s'",
entity_id,
unique_id,
cmd,
args,
cluster.cluster_id,
@ -27,40 +55,214 @@ def parse_and_log_command(entity_id, cluster, tsn, command_id, args):
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:
"""Listener for a Zigbee cluster."""
def __init__(self, entity, cluster):
def __init__(self, cluster, device):
"""Initialize ClusterListener."""
self._entity = entity
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):
"""Handle commands received to this cluster."""
pass
@callback
def attribute_updated(self, attrid, value):
"""Handle attribute updates on this cluster."""
pass
@callback
def zdo_command(self, *args, **kwargs):
"""Handle ZDO commands on this cluster."""
pass
@callback
def zha_send_event(self, cluster, command, args):
"""Relay entity events to hass."""
pass # don't let entities fire events
"""Relay events to hass."""
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):
"""Listener for the OnOff Zigbee cluster."""
name = 'on_off'
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):
"""Handle commands received to this cluster."""
cmd = parse_and_log_command(
self._entity.entity_id,
self.unique_id,
self._cluster,
tsn,
command_id,
@ -68,27 +270,42 @@ class OnOffListener(ClusterListener):
)
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'):
self._entity.set_state(True)
self.attribute_updated(self.ON_OFF, True)
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):
"""Handle attribute updates on this cluster."""
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):
"""Listener for the LevelControl Zigbee cluster."""
name = ATTR_LEVEL
CURRENT_LEVEL = 0
@callback
def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster."""
cmd = parse_and_log_command(
self._entity.entity_id,
self.unique_id,
self._cluster,
tsn,
command_id,
@ -96,21 +313,190 @@ class LevelListener(ClusterListener):
)
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'):
# We should dim slowly -- for now, just step once
rate = args[1]
if args[0] == 0xff:
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'):
# 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):
"""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:
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):
@ -143,3 +529,137 @@ class EventRelayListener(ClusterListener):
self._cluster.server_commands.get(command_id)[0],
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/
"""
import logging
import time
from homeassistant.helpers import entity
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."""
def __init__(self, device, manufacturer, model, application_listener,
keepalive_interval=7200, **kwargs):
def __init__(self, zha_device, listeners, keepalive_interval=7200,
**kwargs):
"""Init ZHA endpoint entity."""
self._device_state_attributes = {
'nwk': '0x{0:04x}'.format(device.nwk),
'ieee': str(device.ieee),
'lqi': device.lqi,
'rssi': device.rssi,
}
ieee = device.ieee
ieee = zha_device.ieee
ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
if manufacturer is not None and model is not None:
self._unique_id = "{}_{}_{}".format(
slugify(manufacturer),
slugify(model),
unique_id = None
if zha_device.manufacturer is not None and \
zha_device.model is not None:
unique_id = "{}_{}_{}".format(
slugify(zha_device.manufacturer),
slugify(zha_device.model),
ieeetail,
)
self._device_state_attributes['friendly_name'] = "{} {}".format(
manufacturer,
model,
)
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
application_listener.register_entity(ieee, self)
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return self._unique_id
self._device_state_attributes.update({
'nwk': '0x{0:04x}'.format(zha_device.nwk),
'ieee': str(zha_device.ieee),
'lqi': zha_device.lqi,
'rssi': zha_device.rssi,
})
self._should_poll = True
self._battery_listener = self.cluster_listeners.get(LISTENER_BATTERY)
@property
def state(self) -> str:
"""Return the state of the entity."""
return self._state
@property
def available(self):
"""Return True if device is available."""
return self._zha_device.available
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
update_time = None
if self._device.last_seen is not None and self._state == 'offline':
time_struct = time.localtime(self._device.last_seen)
device = self._zha_device
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)
self._device_state_attributes['last_seen'] = update_time
if ('last_seen' in self._device_state_attributes and
self._state != 'offline'):
self.available):
del self._device_state_attributes['last_seen']
self._device_state_attributes['lqi'] = self._device.lqi
self._device_state_attributes['rssi'] = self._device.rssi
self._device_state_attributes['lqi'] = device.lqi
self._device_state_attributes['rssi'] = device.rssi
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):
"""Handle polling."""
if self._device.last_seen is None:
self._state = 'offline'
if self._zha_device.last_seen is None:
self._zha_device.update_available(False)
else:
difference = time.time() - self._device.last_seen
difference = time.time() - self._zha_device.last_seen
if difference > self._keepalive_interval:
self._state = 'offline'
self._zha_device.update_available(False)
self._state = None
else:
self._zha_device.update_available(True)
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
https://home-assistant.io/components/zha/
"""
import asyncio
import logging
from random import uniform
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import callback
import logging
from homeassistant.helpers import entity
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import slugify
from .core.const import (
DATA_ZHA, DATA_ZHA_BRIDGE_ID, DOMAIN, ATTR_CLUSTER_ID, ATTR_ATTRIBUTE,
ATTR_VALUE, ATTR_MANUFACTURER, ATTR_COMMAND, SERVER, ATTR_COMMAND_TYPE,
ATTR_ARGS, IN, OUT, CLIENT_COMMANDS, SERVER_COMMANDS)
from .core.helpers import bind_configure_reporting
DOMAIN, ATTR_MANUFACTURER, DATA_ZHA, DATA_ZHA_BRIDGE_ID, MODEL, NAME,
SIGNAL_REMOVE
)
_LOGGER = logging.getLogger(__name__)
@ -29,287 +27,155 @@ class ZhaEntity(entity.Entity):
_domain = None # Must be overridden by subclasses
def __init__(self, endpoint, in_clusters, out_clusters, manufacturer,
model, application_listener, unique_id, new_join=False,
**kwargs):
def __init__(self, unique_id, zha_device, listeners,
skip_entity_id=False, **kwargs):
"""Init ZHA entity."""
self._device_state_attributes = {}
self._name = None
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._force_update = False
self._should_poll = False
self._unique_id = unique_id
# Normally the entity itself is the listener. Sub-classes may set this
# to a dict of cluster ID -> listener to receive messages for specific
# clusters separately
self._in_listeners = {}
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
self._name = None
if zha_device.manufacturer and zha_device.model is not None:
self._name = "{} {}".format(
zha_device.manufacturer,
zha_device.model
)
_LOGGER.debug(
'set: %s for attr: %s to cluster: %s for entity: %s - res: %s',
value,
attribute,
cluster_id,
self.entity_id,
response
)
return response
except DeliveryError as exc:
_LOGGER.debug(
'failed to set attribute: %s %s %s %s %s',
'{}: {}'.format(ATTR_VALUE, value),
'{}: {}'.format(ATTR_ATTRIBUTE, attribute),
'{}: {}'.format(ATTR_CLUSTER_ID, cluster_id),
'{}: {}'.format(ATTR_ENTITY_ID, self.entity_id),
exc
)
async def get_cluster_commands(self, cluster_id, cluster_type=IN):
"""Get zigbee commands for specified cluster."""
cluster = await self._get_cluster(cluster_id, cluster_type)
if cluster is None:
return
return {
CLIENT_COMMANDS: cluster.client_commands,
SERVER_COMMANDS: cluster.server_commands,
}
async def issue_cluster_command(self, cluster_id, command, command_type,
args, cluster_type=IN,
manufacturer=None):
"""Issue a command against specified zigbee cluster on this entity."""
cluster = await self._get_cluster(cluster_id, cluster_type)
if cluster is None:
return
response = None
if command_type == SERVER:
response = await cluster.command(command, *args,
manufacturer=manufacturer,
expect_reply=True)
else:
response = await cluster.client_command(command, *args)
_LOGGER.debug(
'Issued cluster command: %s %s %s %s %s %s %s',
'{}: {}'.format(ATTR_CLUSTER_ID, cluster_id),
'{}: {}'.format(ATTR_COMMAND, command),
'{}: {}'.format(ATTR_COMMAND_TYPE, command_type),
'{}: {}'.format(ATTR_ARGS, args),
'{}: {}'.format(ATTR_CLUSTER_ID, cluster_type),
'{}: {}'.format(ATTR_MANUFACTURER, manufacturer),
'{}: {}'.format(ATTR_ENTITY_ID, self.entity_id)
)
return response
async def async_added_to_hass(self):
"""Handle entity addition to hass.
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
if not skip_entity_id:
ieee = zha_device.ieee
ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
if zha_device.manufacturer and zha_device.model is not None:
self.entity_id = "{}.{}_{}_{}_{}{}".format(
self._domain,
slugify(zha_device.manufacturer),
slugify(zha_device.model),
ieeetail,
listeners[0].cluster.endpoint.endpoint_id,
kwargs.get(ENTITY_SUFFIX, ''),
)
skip_bind = True
await asyncio.sleep(uniform(0.1, 0.5))
_LOGGER.debug("%s: finished configuration", self.entity_id)
def _get_cluster_from_report_config(self, cluster_key):
"""Parse an entry from zcl_reporting_config dict."""
from zigpy.zcl import Cluster as Zcl_Cluster
cluster = None
if isinstance(cluster_key, Zcl_Cluster):
cluster = cluster_key
elif isinstance(cluster_key, str):
cluster = getattr(self._endpoint, cluster_key, None)
elif isinstance(cluster_key, int):
if cluster_key in self._in_clusters:
cluster = self._in_clusters[cluster_key]
elif cluster_key in self._out_clusters:
cluster = self._out_clusters[cluster_key]
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
else:
self.entity_id = "{}.zha_{}_{}{}".format(
self._domain,
ieeetail,
listeners[0].cluster.endpoint.endpoint_id,
kwargs.get(ENTITY_SUFFIX, ''),
)
self._state = None
self._device_state_attributes = {}
self._zha_device = zha_device
self.cluster_listeners = {}
# this will get flipped to false once we enable the feature after the
# reorg is merged
self._available = True
self._component = kwargs['component']
self._unsubs = []
for listener in listeners:
self.cluster_listeners[listener.name] = listener
@property
def name(self):
"""Return Entity's default 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
def unique_id(self) -> str:
"""Return a 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
def device_state_attributes(self):
"""Return device specific state attributes."""
return self._device_state_attributes
@property
def force_update(self) -> bool:
"""Force update this entity."""
return self._force_update
@property
def should_poll(self) -> bool:
"""Let ZHA handle polling."""
return False
@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
"""Poll state from device."""
return self._should_poll
@property
def device_info(self):
"""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 {
'connections': {(CONNECTION_ZIGBEE, ieee)},
'identifiers': {(DOMAIN, ieee)},
ATTR_MANUFACTURER: self._endpoint.manufacturer,
'model': self._endpoint.model,
'name': self.name or ieee,
ATTR_MANUFACTURER: zha_device_info[ATTR_MANUFACTURER],
MODEL: zha_device_info[MODEL],
NAME: zha_device_info[NAME],
'via_hub': (DOMAIN, self.hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID]),
}
@callback
def zha_send_event(self, cluster, command, args):
"""Relay entity events to hass."""
pass # don't relay events from entities
@property
def available(self):
"""Return entity availability."""
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,
FanEntity)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .core import helpers
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
DEPENDENCIES = ['zha']
@ -79,19 +80,17 @@ class ZhaFan(ZhaEntity, FanEntity):
"""Representation of a ZHA fan."""
_domain = DOMAIN
value_attribute = 0 # fan_mode
@property
def zcl_reporting_config(self) -> dict:
"""Return a dict of attribute reporting configuration."""
return {
self.cluster: {self.value_attribute: REPORT_CONFIG_OP}
}
def __init__(self, unique_id, zha_device, listeners, **kwargs):
"""Init this sensor."""
super().__init__(unique_id, zha_device, listeners, **kwargs)
self._fan_listener = self.cluster_listeners.get(LISTENER_FAN)
@property
def cluster(self):
"""Fan ZCL Cluster."""
return self._endpoint.fan
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._fan_listener, SIGNAL_ATTR_UPDATED, self.async_set_state)
@property
def supported_features(self) -> int:
@ -115,6 +114,16 @@ class ZhaFan(ZhaEntity, FanEntity):
return False
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:
"""Turn the entity on."""
if speed is None:
@ -128,31 +137,5 @@ class ZhaFan(ZhaEntity, FanEntity):
async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan."""
from zigpy.exceptions import DeliveryError
try:
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()
await self._fan_listener.async_set_speed(SPEED_TO_VALUE[speed])
self.async_set_state(speed)

View File

@ -9,14 +9,12 @@ import logging
from homeassistant.components import light
from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.util.color as color_util
from .core import helpers
from .core.const import (
DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT,
REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW)
from .const import (
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, LISTENER_COLOR,
LISTENER_ON_OFF, LISTENER_LEVEL, SIGNAL_ATTR_UPDATED, SIGNAL_SET_LEVEL
)
from .entity import ZhaEntity
from .core.listeners import (
OnOffListener, LevelListener
)
_LOGGER = logging.getLogger(__name__)
@ -58,26 +56,6 @@ async def _async_setup_entities(hass, config_entry, async_add_entities,
"""Set up the ZHA lights."""
entities = []
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)
entities.append(zha_light)
@ -89,34 +67,24 @@ class Light(ZhaEntity, light.Light):
_domain = light.DOMAIN
def __init__(self, **kwargs):
def __init__(self, unique_id, zha_device, listeners, **kwargs):
"""Initialize the ZHA light."""
super().__init__(**kwargs)
super().__init__(unique_id, zha_device, listeners, **kwargs)
self._supported_features = 0
self._color_temp = None
self._hs_color = None
self._brightness = None
from zigpy.zcl.clusters.general import OnOff, LevelControl
self._in_listeners = {
OnOff.cluster_id: OnOffListener(
self,
self._in_clusters[OnOff.cluster_id]
),
}
self._on_off_listener = self.cluster_listeners.get(LISTENER_ON_OFF)
self._level_listener = self.cluster_listeners.get(LISTENER_LEVEL)
self._color_listener = self.cluster_listeners.get(LISTENER_COLOR)
if LevelControl.cluster_id in self._in_clusters:
if self._level_listener:
self._supported_features |= light.SUPPORT_BRIGHTNESS
self._supported_features |= light.SUPPORT_TRANSITION
self._brightness = 0
self._in_listeners.update({
LevelControl.cluster_id: LevelListener(
self,
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 self._color_listener:
color_capabilities = self._color_listener.get_color_capabilities()
if color_capabilities & CAPABILITIES_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._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
def is_on(self) -> bool:
"""Return true if entity is on."""
if self._state is None:
return False
return bool(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()
return self._state
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
"""Return the brightness of this light."""
return self._brightness
@property
def device_state_attributes(self):
"""Return state attributes."""
return self.state_attributes
def set_level(self, value):
"""Set the brightness of this light between 0..255."""
if value < 0 or value > 255:
return
value = max(0, min(255, value))
self._brightness = value
self.async_schedule_update_ha_state()
self.async_set_state(value)
@property
def hs_color(self):
@ -265,40 +130,82 @@ class Light(ZhaEntity, light.Light):
"""Flag supported features."""
return self._supported_features
async def async_update(self):
"""Retrieve latest state."""
result = await helpers.safe_read(self._endpoint.on_off, ['on_off'],
allow_cache=False,
only_cache=(not self._initialized))
self._state = result.get('on_off', self._state)
def async_set_state(self, state):
"""Set the state."""
self._state = bool(state)
self.async_schedule_update_ha_state()
if self._supported_features & light.SUPPORT_BRIGHTNESS:
result = await helpers.safe_read(self._endpoint.level,
['current_level'],
allow_cache=False,
only_cache=(
not self._initialized
))
self._brightness = result.get('current_level', self._brightness)
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._on_off_listener, SIGNAL_ATTR_UPDATED, self.async_set_state)
if self._level_listener:
await self.async_accept_signal(
self._level_listener, SIGNAL_SET_LEVEL, self.set_level)
if self._supported_features & light.SUPPORT_COLOR_TEMP:
result = await helpers.safe_read(self._endpoint.light_color,
['color_temperature'],
allow_cache=False,
only_cache=(
not self._initialized
))
self._color_temp = result.get('color_temperature',
self._color_temp)
async def async_turn_on(self, **kwargs):
"""Turn the entity on."""
duration = kwargs.get(light.ATTR_TRANSITION, DEFAULT_DURATION)
duration = duration * 10 # tenths of s
if self._supported_features & light.SUPPORT_COLOR:
result = await helpers.safe_read(self._endpoint.light_color,
['current_x', 'current_y'],
allow_cache=False,
only_cache=(
not self._initialized
))
if 'current_x' in result and 'current_y' in result:
xy_color = (round(result['current_x']/65535, 3),
round(result['current_y']/65535, 3))
self._hs_color = color_util.color_xy_to_hs(*xy_color)
if light.ATTR_COLOR_TEMP in kwargs and \
self.supported_features & light.SUPPORT_COLOR_TEMP:
temperature = kwargs[light.ATTR_COLOR_TEMP]
success = await self._color_listener.move_to_color_temp(
temperature, duration)
if not success:
return
self._color_temp = temperature
if light.ATTR_HS_COLOR in kwargs and \
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.const import TEMP_CELSIUS
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 (
DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_MAX_INT,
REPORT_CONFIG_MIN_INT, REPORT_CONFIG_RPT_CHANGE, ZHA_DISCOVERY_NEW)
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, HUMIDITY, TEMPERATURE,
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
_LOGGER = logging.getLogger(__name__)
@ -21,6 +21,73 @@ _LOGGER = logging.getLogger(__name__)
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,
discovery_info=None):
"""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):
"""Create ZHA sensors factory."""
from zigpy.zcl.clusters.measurement import (
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
return Sensor(**discovery_info)
class Sensor(ZhaEntity):
"""Base ZHA sensor."""
_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):
"""Init ZHA Sensor instance."""
super().__init__(**kwargs)
self._cluster = list(kwargs['in_clusters'].values())[0]
def __init__(self, unique_id, zha_device, listeners, **kwargs):
"""Init this sensor."""
super().__init__(unique_id, zha_device, listeners, **kwargs)
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
def zcl_reporting_config(self) -> dict:
"""Return a dict of attribute reporting configuration."""
return {
self.cluster: {self.value_attribute: self.report_config}
}
@property
def cluster(self):
"""Return Sensor's cluster."""
return self._cluster
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return self._unit
@property
def state(self) -> str:
"""Return the state of the entity."""
if self._state is None:
return None
if isinstance(self._state, float):
return str(round(self._state, 2))
return self._state
def attribute_updated(self, attribute, value):
"""Handle attribute update from device."""
_LOGGER.debug("Attribute updated: %s %s %s", self, attribute, value)
if attribute == self.value_attribute:
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)
def async_set_state(self, state):
"""Handle state update from listener."""
self._state = self._formatter_function(state)
self.async_schedule_update_ha_state()

View File

@ -8,9 +8,10 @@ import logging
from homeassistant.components.switch import DOMAIN, SwitchDevice
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .core import helpers
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
_LOGGER = logging.getLogger(__name__)
@ -55,69 +56,39 @@ class Switch(ZhaEntity, SwitchDevice):
"""ZHA switch."""
_domain = DOMAIN
value_attribute = 0
def attribute_updated(self, attribute, value):
"""Handle attribute update from device."""
cluster = self._endpoint.on_off
attr_name = cluster.attributes.get(attribute, [attribute])[0]
_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
def __init__(self, **kwargs):
"""Initialize the ZHA switch."""
super().__init__(**kwargs)
self._on_off_listener = self.cluster_listeners.get(LISTENER_ON_OFF)
@property
def is_on(self) -> bool:
"""Return if the switch is on based on the statemachine."""
if self._state is None:
return False
return bool(self._state)
return self._state
async def async_turn_on(self, **kwargs):
"""Turn the entity on."""
from zigpy.exceptions import DeliveryError
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()
await self._on_off_listener.on()
async def async_turn_off(self, **kwargs):
"""Turn the entity off."""
from zigpy.exceptions import DeliveryError
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
await self._on_off_listener.off()
self._state = 0
def async_set_state(self, state):
"""Handle state update from listener."""
self._state = bool(state)
self.async_schedule_update_ha_state()
async def async_update(self):
"""Retrieve latest state."""
result = await helpers.safe_read(self.cluster,
['on_off'],
allow_cache=False,
only_cache=(not self._initialized))
self._state = result.get('on_off', self._state)
@property
def device_state_attributes(self):
"""Return state attributes."""
return self.state_attributes
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._on_off_listener, SIGNAL_ATTR_UPDATED, self.async_set_state)

View File

@ -3,9 +3,12 @@ from unittest.mock import patch
import pytest
from homeassistant import config_entries
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 establish_device_mappings
from homeassistant.components.zha.core.listeners \
import populate_listener_registry
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
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, {})

View File

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

View File

@ -26,6 +26,7 @@ async def test_fan(hass, config_entry, zha_gateway):
# load up fan domain
await hass.config_entries.async_forward_entry_setup(
config_entry, DOMAIN)
await zha_gateway.accept_zigbee_messages({})
await hass.async_block_till_done()
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
await hass.config_entries.async_forward_entry_setup(
config_entry, DOMAIN)
await zha_gateway.accept_zigbee_messages({})
await hass.async_block_till_done()
# 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
await hass.config_entries.async_forward_entry_setup(
config_entry, DOMAIN)
await zha_gateway.accept_zigbee_messages({})
await hass.async_block_till_done()
# 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
await hass.config_entries.async_forward_entry_setup(
config_entry, DOMAIN)
await zha_gateway.accept_zigbee_messages({})
await hass.async_block_till_done()
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()
assert hass.states.get(entity_id).state == STATE_OFF
# turn on from HA
with patch(
'zigpy.zcl.Cluster.request',
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(
False, ON, (), expect_reply=True, manufacturer=None)
# turn off from HA
with patch(
'zigpy.zcl.Cluster.request',
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(
False, OFF, (), expect_reply=True, manufacturer=None)
# test joining a new switch to the network and HA
await async_test_device_join(
hass, zha_gateway, OnOff.cluster_id, DOMAIN, expected_state=STATE_OFF)