Refactor ZHA listeners into channels (#21196)

* refactor listeners to channels

* update coveragerc
This commit is contained in:
David F. Mulcahey 2019-02-19 12:58:22 -05:00 committed by Paulus Schoutsen
parent fe4a2b5b31
commit 3be8178035
28 changed files with 1037 additions and 899 deletions

View file

@ -669,11 +669,11 @@ omit =
homeassistant/components/zha/__init__.py homeassistant/components/zha/__init__.py
homeassistant/components/zha/api.py homeassistant/components/zha/api.py
homeassistant/components/zha/const.py homeassistant/components/zha/const.py
homeassistant/components/zha/core/channels/*
homeassistant/components/zha/core/const.py homeassistant/components/zha/core/const.py
homeassistant/components/zha/core/device.py homeassistant/components/zha/core/device.py
homeassistant/components/zha/core/gateway.py homeassistant/components/zha/core/gateway.py
homeassistant/components/zha/core/helpers.py homeassistant/components/zha/core/helpers.py
homeassistant/components/zha/core/listeners.py
homeassistant/components/zha/device_entity.py homeassistant/components/zha/device_entity.py
homeassistant/components/zha/entity.py homeassistant/components/zha/entity.py
homeassistant/components/zha/light.py homeassistant/components/zha/light.py

View file

@ -26,7 +26,7 @@ from .core.const import (
DATA_ZHA_RADIO, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME, DATA_ZHA_RADIO, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME,
DEFAULT_RADIO_TYPE, DOMAIN, RadioType, DATA_ZHA_CORE_EVENTS, ENABLE_QUIRKS) DEFAULT_RADIO_TYPE, DOMAIN, RadioType, DATA_ZHA_CORE_EVENTS, ENABLE_QUIRKS)
from .core.gateway import establish_device_mappings from .core.gateway import establish_device_mappings
from .core.listeners import populate_listener_registry from .core.channels.registry import populate_channel_registry
REQUIREMENTS = [ REQUIREMENTS = [
'bellows==0.7.0', 'bellows==0.7.0',
@ -90,7 +90,7 @@ async def async_setup_entry(hass, config_entry):
Will automatically load components to support devices found on the network. Will automatically load components to support devices found on the network.
""" """
establish_device_mappings() establish_device_mappings()
populate_listener_registry() populate_channel_registry()
for component in COMPONENTS: for component in COMPONENTS:
hass.data[DATA_ZHA][component] = ( hass.data[DATA_ZHA][component] = (

View file

@ -9,9 +9,9 @@ import logging
from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .core.const import ( from .core.const import (
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, LISTENER_ON_OFF, DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, ON_OFF_CHANNEL,
LISTENER_LEVEL, LISTENER_ZONE, SIGNAL_ATTR_UPDATED, SIGNAL_MOVE_LEVEL, LEVEL_CHANNEL, ZONE_CHANNEL, SIGNAL_ATTR_UPDATED, SIGNAL_MOVE_LEVEL,
SIGNAL_SET_LEVEL, LISTENER_ATTRIBUTE, UNKNOWN, OPENING, ZONE, OCCUPANCY, SIGNAL_SET_LEVEL, ATTRIBUTE_CHANNEL, UNKNOWN, OPENING, ZONE, OCCUPANCY,
ATTR_LEVEL, SENSOR_TYPE) ATTR_LEVEL, SENSOR_TYPE)
from .entity import ZhaEntity from .entity import ZhaEntity
@ -30,9 +30,9 @@ CLASS_MAPPING = {
} }
async def get_ias_device_class(listener): async def get_ias_device_class(channel):
"""Get the HA device class from the listener.""" """Get the HA device class from the channel."""
zone_type = await listener.get_attribute_value('zone_type') zone_type = await channel.get_attribute_value('zone_type')
return CLASS_MAPPING.get(zone_type) return CLASS_MAPPING.get(zone_type)
@ -87,10 +87,10 @@ class BinarySensor(ZhaEntity, BinarySensorDevice):
"""Initialize the ZHA binary sensor.""" """Initialize the ZHA binary sensor."""
super().__init__(**kwargs) super().__init__(**kwargs)
self._device_state_attributes = {} self._device_state_attributes = {}
self._zone_listener = self.cluster_listeners.get(LISTENER_ZONE) self._zone_channel = self.cluster_channels.get(ZONE_CHANNEL)
self._on_off_listener = self.cluster_listeners.get(LISTENER_ON_OFF) self._on_off_channel = self.cluster_channels.get(ON_OFF_CHANNEL)
self._level_listener = self.cluster_listeners.get(LISTENER_LEVEL) self._level_channel = self.cluster_channels.get(LEVEL_CHANNEL)
self._attr_listener = self.cluster_listeners.get(LISTENER_ATTRIBUTE) self._attr_channel = self.cluster_channels.get(ATTRIBUTE_CHANNEL)
self._zha_sensor_type = kwargs[SENSOR_TYPE] self._zha_sensor_type = kwargs[SENSOR_TYPE]
self._level = None self._level = None
@ -99,31 +99,31 @@ class BinarySensor(ZhaEntity, BinarySensorDevice):
device_class_supplier = DEVICE_CLASS_REGISTRY.get( device_class_supplier = DEVICE_CLASS_REGISTRY.get(
self._zha_sensor_type) self._zha_sensor_type)
if callable(device_class_supplier): if callable(device_class_supplier):
listener = self.cluster_listeners.get(self._zha_sensor_type) channel = self.cluster_channels.get(self._zha_sensor_type)
if listener is None: if channel is None:
return None return None
return await device_class_supplier(listener) return await device_class_supplier(channel)
return device_class_supplier return device_class_supplier
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Run when about to be added to hass.""" """Run when about to be added to hass."""
self._device_class = await self._determine_device_class() self._device_class = await self._determine_device_class()
await super().async_added_to_hass() await super().async_added_to_hass()
if self._level_listener: if self._level_channel:
await self.async_accept_signal( await self.async_accept_signal(
self._level_listener, SIGNAL_SET_LEVEL, self.set_level) self._level_channel, SIGNAL_SET_LEVEL, self.set_level)
await self.async_accept_signal( await self.async_accept_signal(
self._level_listener, SIGNAL_MOVE_LEVEL, self.move_level) self._level_channel, SIGNAL_MOVE_LEVEL, self.move_level)
if self._on_off_listener: if self._on_off_channel:
await self.async_accept_signal( await self.async_accept_signal(
self._on_off_listener, SIGNAL_ATTR_UPDATED, self._on_off_channel, SIGNAL_ATTR_UPDATED,
self.async_set_state) self.async_set_state)
if self._zone_listener: if self._zone_channel:
await self.async_accept_signal( await self.async_accept_signal(
self._zone_listener, SIGNAL_ATTR_UPDATED, self.async_set_state) self._zone_channel, SIGNAL_ATTR_UPDATED, self.async_set_state)
if self._attr_listener: if self._attr_channel:
await self.async_accept_signal( await self.async_accept_signal(
self._attr_listener, SIGNAL_ATTR_UPDATED, self.async_set_state) self._attr_channel, SIGNAL_ATTR_UPDATED, self.async_set_state)
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
@ -160,7 +160,7 @@ class BinarySensor(ZhaEntity, BinarySensorDevice):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the device state attributes.""" """Return the device state attributes."""
if self._level_listener is not None: if self._level_channel is not None:
self._device_state_attributes.update({ self._device_state_attributes.update({
ATTR_LEVEL: self._state and self._level or 0 ATTR_LEVEL: self._state and self._level or 0
}) })

View file

@ -8,6 +8,3 @@ https://home-assistant.io/components/zha/
# flake8: noqa # flake8: noqa
from .device import ZHADevice from .device import ZHADevice
from .gateway import ZHAGateway from .gateway import ZHAGateway
from .listeners import (
ClusterListener, AttributeListener, OnOffListener, LevelListener,
IASZoneListener, ActivePowerListener, BatteryListener, EventRelayListener)

View file

@ -0,0 +1,308 @@
"""
Channels module for Zigbee Home Automation.
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 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,
ATTRIBUTE_CHANNEL, EVENT_RELAY_CHANNEL
)
ZIGBEE_CHANNEL_REGISTRY = {}
_LOGGER = logging.getLogger(__name__)
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'",
unique_id,
cmd,
args,
cluster.cluster_id,
tsn
)
return cmd
def decorate_command(channel, 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",
channel.unique_id,
command.__name__,
"{}: {}".format("with args", args),
"{}: {}".format("with kwargs", kwds),
"{}: {}".format("and result", result))
if isinstance(result, bool):
return result
return result[1] is Status.SUCCESS
except DeliveryError:
_LOGGER.debug("%s: command failed: %s", channel.unique_id,
command.__name__)
return False
return wrapper
class ChannelStatus(Enum):
"""Status of a channel."""
CREATED = 1
CONFIGURED = 2
INITIALIZED = 3
class ZigbeeChannel:
"""Base channel for a Zigbee cluster."""
def __init__(self, cluster, device):
"""Initialize ZigbeeChannel."""
self.name = 'channel_{}'.format(cluster.cluster_id)
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 = ChannelStatus.CREATED
self._cluster.add_listener(self)
@property
def unique_id(self):
"""Return the unique id for this channel."""
return self._unique_id
@property
def cluster(self):
"""Return the zigpy cluster for this channel."""
return self._cluster
@property
def device(self):
"""Return the device this channel is linked to."""
return self._zha_device
@property
def status(self):
"""Return the status of the channel."""
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 channel configuration",
self._unique_id
)
self._status = ChannelStatus.CONFIGURED
async def async_initialize(self, from_cache):
"""Initialize channel."""
self._status = ChannelStatus.INITIALIZED
@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 events to hass."""
self._zha_device.hass.bus.async_fire(
'zha_event',
{
'unique_id': self._unique_id,
'device_ieee': str(self._zha_device.ieee),
'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 AttributeListeningChannel(ZigbeeChannel):
"""Channel for attribute reports from the cluster."""
def __init__(self, cluster, device):
"""Initialize AttributeListeningChannel."""
super().__init__(cluster, device)
self.name = ATTRIBUTE_CHANNEL
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 ZDOChannel:
"""Channel for ZDO events."""
def __init__(self, cluster, device):
"""Initialize ZDOChannel."""
self.name = 'zdo'
self._cluster = cluster
self._zha_device = device
self._status = ChannelStatus.CREATED
self._unique_id = "{}_ZDO".format(device.name)
self._cluster.add_listener(self)
@property
def unique_id(self):
"""Return the unique id for this channel."""
return self._unique_id
@property
def cluster(self):
"""Return the aigpy cluster for this channel."""
return self._cluster
@property
def status(self):
"""Return the status of the channel."""
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 async_initialize(self, from_cache):
"""Initialize channel."""
self._status = ChannelStatus.INITIALIZED
async def async_configure(self):
"""Configure channel."""
self._status = ChannelStatus.CONFIGURED
class EventRelayChannel(ZigbeeChannel):
"""Event relay that can be attached to zigbee clusters."""
def __init__(self, cluster, device):
"""Initialize EventRelayChannel."""
super().__init__(cluster, device)
self.name = EVENT_RELAY_CHANNEL
@callback
def attribute_updated(self, attrid, value):
"""Handle an attribute updated on this cluster."""
self.zha_send_event(
self._cluster,
SIGNAL_ATTR_UPDATED,
{
'attribute_id': attrid,
'attribute_name': self._cluster.attributes.get(
attrid,
['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

@ -0,0 +1,9 @@
"""
Closures channels module for Zigbee Home Automation.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/
"""
import logging
_LOGGER = logging.getLogger(__name__)

View file

@ -0,0 +1,202 @@
"""
General channels module 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 callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
from . import ZigbeeChannel, parse_and_log_command
from ..helpers import get_attr_id_by_name
from ..const import (
SIGNAL_ATTR_UPDATED,
SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL, SIGNAL_STATE_ATTR, BASIC_CHANNEL,
ON_OFF_CHANNEL, LEVEL_CHANNEL, POWER_CONFIGURATION_CHANNEL
)
_LOGGER = logging.getLogger(__name__)
class OnOffChannel(ZigbeeChannel):
"""Channel for the OnOff Zigbee cluster."""
ON_OFF = 0
def __init__(self, cluster, device):
"""Initialize OnOffChannel."""
super().__init__(cluster, device)
self.name = ON_OFF_CHANNEL
self._state = None
@callback
def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster."""
cmd = parse_and_log_command(
self.unique_id,
self._cluster,
tsn,
command_id,
args
)
if cmd in ('off', 'off_with_effect'):
self.attribute_updated(self.ON_OFF, False)
elif cmd in ('on', 'on_with_recall_global_scene', 'on_with_timed_off'):
self.attribute_updated(self.ON_OFF, True)
elif cmd == 'toggle':
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:
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 channel."""
self._state = bool(
await self.get_attribute_value(self.ON_OFF, from_cache=from_cache))
await super().async_initialize(from_cache)
class LevelControlChannel(ZigbeeChannel):
"""Channel for the LevelControl Zigbee cluster."""
CURRENT_LEVEL = 0
def __init__(self, cluster, device):
"""Initialize LevelControlChannel."""
super().__init__(cluster, device)
self.name = LEVEL_CHANNEL
@callback
def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster."""
cmd = parse_and_log_command(
self.unique_id,
self._cluster,
tsn,
command_id,
args
)
if cmd in ('move_to_level', 'move_to_level_with_on_off'):
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.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.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.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 channel."""
await self.get_attribute_value(
self.CURRENT_LEVEL, from_cache=from_cache)
await super().async_initialize(from_cache)
class BasicChannel(ZigbeeChannel):
"""Channel to interact with the basic cluster."""
BATTERY = 3
POWER_SOURCES = {
0: 'Unknown',
1: 'Mains (single phase)',
2: 'Mains (3 phase)',
BATTERY: 'Battery',
4: 'DC source',
5: 'Emergency mains constantly powered',
6: 'Emergency mains and transfer switch'
}
def __init__(self, cluster, device):
"""Initialize BasicChannel."""
super().__init__(cluster, device)
self.name = BASIC_CHANNEL
self._power_source = None
async def async_configure(self):
"""Configure this channel."""
await super().async_configure()
await self.async_initialize(False)
async def async_initialize(self, from_cache):
"""Initialize channel."""
self._power_source = await self.get_attribute_value(
'power_source', from_cache=from_cache)
await super().async_initialize(from_cache)
def get_power_source(self):
"""Get the power source."""
return self._power_source
class PowerConfigurationChannel(ZigbeeChannel):
"""Channel for the zigbee power configuration cluster."""
def __init__(self, cluster, device):
"""Initialize PowerConfigurationChannel."""
super().__init__(cluster, device)
self.name = POWER_CONFIGURATION_CHANNEL
@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 channel."""
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)

View file

@ -0,0 +1,40 @@
"""
Home automation channels module 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.helpers.dispatcher import async_dispatcher_send
from . import AttributeListeningChannel
from ..const import SIGNAL_ATTR_UPDATED, ELECTRICAL_MEASUREMENT_CHANNEL
_LOGGER = logging.getLogger(__name__)
class ElectricalMeasurementChannel(AttributeListeningChannel):
"""Channel that polls active power level."""
def __init__(self, cluster, device):
"""Initialize ElectricalMeasurementChannel."""
super().__init__(cluster, device)
self.name = ELECTRICAL_MEASUREMENT_CHANNEL
async def async_update(self):
"""Retrieve latest state."""
_LOGGER.debug("%s async_update", self.unique_id)
# This is a polling channel. Don't allow cache.
result = await self.get_attribute_value(
ELECTRICAL_MEASUREMENT_CHANNEL, 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 channel."""
await self.get_attribute_value(
ELECTRICAL_MEASUREMENT_CHANNEL, from_cache=from_cache)
await super().async_initialize(from_cache)

View file

@ -0,0 +1,62 @@
"""
HVAC channels module 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 callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
from . import ZigbeeChannel
from ..const import FAN_CHANNEL, SIGNAL_ATTR_UPDATED
_LOGGER = logging.getLogger(__name__)
class FanChannel(ZigbeeChannel):
"""Fan channel."""
_value_attribute = 0
def __init__(self, cluster, device):
"""Initialize FanChannel."""
super().__init__(cluster, device)
self.name = FAN_CHANNEL
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
)
@callback
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 channel."""
await self.get_attribute_value(
self._value_attribute, from_cache=from_cache)
await super().async_initialize(from_cache)

View file

@ -0,0 +1,48 @@
"""
Lighting channels module 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 . import ZigbeeChannel
from ..const import COLOR_CHANNEL
_LOGGER = logging.getLogger(__name__)
class ColorChannel(ZigbeeChannel):
"""Color channel."""
CAPABILITIES_COLOR_XY = 0x08
CAPABILITIES_COLOR_TEMP = 0x10
UNSUPPORTED_ATTRIBUTE = 0x86
def __init__(self, cluster, device):
"""Initialize ColorChannel."""
super().__init__(cluster, device)
self.name = COLOR_CHANNEL
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 channel."""
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)

View file

@ -0,0 +1,9 @@
"""
Lightlink channels module for Zigbee Home Automation.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/
"""
import logging
_LOGGER = logging.getLogger(__name__)

View file

@ -0,0 +1,9 @@
"""
Manufacturer specific channels module for Zigbee Home Automation.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/
"""
import logging
_LOGGER = logging.getLogger(__name__)

View file

@ -0,0 +1,9 @@
"""
Measurement channels module for Zigbee Home Automation.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/
"""
import logging
_LOGGER = logging.getLogger(__name__)

View file

@ -0,0 +1,9 @@
"""
Protocol channels module for Zigbee Home Automation.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/
"""
import logging
_LOGGER = logging.getLogger(__name__)

View file

@ -0,0 +1,46 @@
"""
Channel registry module for Zigbee Home Automation.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/
"""
from . import ZigbeeChannel
from .general import (
OnOffChannel, LevelControlChannel, PowerConfigurationChannel, BasicChannel
)
from .homeautomation import ElectricalMeasurementChannel
from .hvac import FanChannel
from .lighting import ColorChannel
from .security import IASZoneChannel
ZIGBEE_CHANNEL_REGISTRY = {}
def populate_channel_registry():
"""Populate the channel registry."""
from zigpy import zcl
ZIGBEE_CHANNEL_REGISTRY.update({
zcl.clusters.general.Alarms.cluster_id: ZigbeeChannel,
zcl.clusters.general.Commissioning.cluster_id: ZigbeeChannel,
zcl.clusters.general.Identify.cluster_id: ZigbeeChannel,
zcl.clusters.general.Groups.cluster_id: ZigbeeChannel,
zcl.clusters.general.Scenes.cluster_id: ZigbeeChannel,
zcl.clusters.general.Partition.cluster_id: ZigbeeChannel,
zcl.clusters.general.Ota.cluster_id: ZigbeeChannel,
zcl.clusters.general.PowerProfile.cluster_id: ZigbeeChannel,
zcl.clusters.general.ApplianceControl.cluster_id: ZigbeeChannel,
zcl.clusters.general.PollControl.cluster_id: ZigbeeChannel,
zcl.clusters.general.GreenPowerProxy.cluster_id: ZigbeeChannel,
zcl.clusters.general.OnOffConfiguration.cluster_id: ZigbeeChannel,
zcl.clusters.general.OnOff.cluster_id: OnOffChannel,
zcl.clusters.general.LevelControl.cluster_id: LevelControlChannel,
zcl.clusters.lighting.Color.cluster_id: ColorChannel,
zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id:
ElectricalMeasurementChannel,
zcl.clusters.general.PowerConfiguration.cluster_id:
PowerConfigurationChannel,
zcl.clusters.general.Basic.cluster_id: BasicChannel,
zcl.clusters.security.IasZone.cluster_id: IASZoneChannel,
zcl.clusters.hvac.Fan.cluster_id: FanChannel,
})

View file

@ -0,0 +1,82 @@
"""
Security channels module 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 callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
from . import ZigbeeChannel
from ..helpers import bind_cluster
from ..const import SIGNAL_ATTR_UPDATED, ZONE_CHANNEL
_LOGGER = logging.getLogger(__name__)
class IASZoneChannel(ZigbeeChannel):
"""Channel for the IASZone Zigbee cluster."""
def __init__(self, cluster, device):
"""Initialize IASZoneChannel."""
super().__init__(cluster, device)
self.name = ZONE_CHANNEL
@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 IASZoneChannel configuration",
self._unique_id)
await bind_cluster(self.unique_id, self._cluster)
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 IASZoneChannel 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 channel."""
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)

View file

@ -0,0 +1,9 @@
"""
Smart energy channels module for Zigbee Home Automation.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/
"""
import logging
_LOGGER = logging.getLogger(__name__)

View file

@ -70,16 +70,16 @@ OCCUPANCY = 'occupancy'
ATTR_LEVEL = 'level' ATTR_LEVEL = 'level'
LISTENER_ON_OFF = 'on_off' ON_OFF_CHANNEL = 'on_off'
LISTENER_ATTRIBUTE = 'attribute' ATTRIBUTE_CHANNEL = 'attribute'
LISTENER_BASIC = 'basic' BASIC_CHANNEL = 'basic'
LISTENER_COLOR = 'color' COLOR_CHANNEL = 'color'
LISTENER_FAN = 'fan' FAN_CHANNEL = 'fan'
LISTENER_LEVEL = ATTR_LEVEL LEVEL_CHANNEL = ATTR_LEVEL
LISTENER_ZONE = 'zone' ZONE_CHANNEL = 'zone'
LISTENER_ACTIVE_POWER = 'active_power' ELECTRICAL_MEASUREMENT_CHANNEL = 'active_power'
LISTENER_BATTERY = 'battery' POWER_CONFIGURATION_CHANNEL = 'battery'
LISTENER_EVENT_RELAY = 'event_relay' EVENT_RELAY_CHANNEL = 'event_relay'
SIGNAL_ATTR_UPDATED = 'attribute_updated' SIGNAL_ATTR_UPDATED = 'attribute_updated'
SIGNAL_MOVE_LEVEL = "move_level" SIGNAL_MOVE_LEVEL = "move_level"

View file

@ -11,13 +11,14 @@ from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send async_dispatcher_connect, async_dispatcher_send
) )
from .const import ( from .const import (
ATTR_MANUFACTURER, LISTENER_BATTERY, SIGNAL_AVAILABLE, IN, OUT, ATTR_MANUFACTURER, POWER_CONFIGURATION_CHANNEL, SIGNAL_AVAILABLE, IN, OUT,
ATTR_CLUSTER_ID, ATTR_ATTRIBUTE, ATTR_VALUE, ATTR_COMMAND, SERVER, ATTR_CLUSTER_ID, ATTR_ATTRIBUTE, ATTR_VALUE, ATTR_COMMAND, SERVER,
ATTR_COMMAND_TYPE, ATTR_ARGS, CLIENT_COMMANDS, SERVER_COMMANDS, ATTR_COMMAND_TYPE, ATTR_ARGS, CLIENT_COMMANDS, SERVER_COMMANDS,
ATTR_ENDPOINT_ID, IEEE, MODEL, NAME, UNKNOWN, QUIRK_APPLIED, ATTR_ENDPOINT_ID, IEEE, MODEL, NAME, UNKNOWN, QUIRK_APPLIED,
QUIRK_CLASS, LISTENER_BASIC QUIRK_CLASS, BASIC_CHANNEL
) )
from .listeners import EventRelayListener, BasicListener from .channels import EventRelayChannel
from .channels.general import BasicChannel
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -38,9 +39,9 @@ class ZHADevice:
self._manufacturer = zigpy_device.endpoints[ept_id].manufacturer self._manufacturer = zigpy_device.endpoints[ept_id].manufacturer
self._model = zigpy_device.endpoints[ept_id].model self._model = zigpy_device.endpoints[ept_id].model
self._zha_gateway = zha_gateway self._zha_gateway = zha_gateway
self.cluster_listeners = {} self.cluster_channels = {}
self._relay_listeners = [] self._relay_channels = []
self._all_listeners = [] self._all_channels = []
self._name = "{} {}".format( self._name = "{} {}".format(
self.manufacturer, self.manufacturer,
self.model self.model
@ -113,9 +114,9 @@ class ZHADevice:
return self._zha_gateway return self._zha_gateway
@property @property
def all_listeners(self): def all_channels(self):
"""Return cluster listeners and relay listeners for device.""" """Return cluster channels and relay channels for device."""
return self._all_listeners return self._all_channels
@property @property
def available_signal(self): def available_signal(self):
@ -156,59 +157,59 @@ class ZHADevice:
QUIRK_CLASS: self.quirk_class QUIRK_CLASS: self.quirk_class
} }
def add_cluster_listener(self, cluster_listener): def add_cluster_channel(self, cluster_channel):
"""Add cluster listener to device.""" """Add cluster channel to device."""
# only keep 1 power listener # only keep 1 power configuration channel
if cluster_listener.name is LISTENER_BATTERY and \ if cluster_channel.name is POWER_CONFIGURATION_CHANNEL and \
LISTENER_BATTERY in self.cluster_listeners: POWER_CONFIGURATION_CHANNEL in self.cluster_channels:
return return
self._all_listeners.append(cluster_listener) self._all_channels.append(cluster_channel)
if isinstance(cluster_listener, EventRelayListener): if isinstance(cluster_channel, EventRelayChannel):
self._relay_listeners.append(cluster_listener) self._relay_channels.append(cluster_channel)
else: else:
self.cluster_listeners[cluster_listener.name] = cluster_listener self.cluster_channels[cluster_channel.name] = cluster_channel
async def async_configure(self): async def async_configure(self):
"""Configure the device.""" """Configure the device."""
_LOGGER.debug('%s: started configuration', self.name) _LOGGER.debug('%s: started configuration', self.name)
await self._execute_listener_tasks('async_configure') await self._execute_channel_tasks('async_configure')
_LOGGER.debug('%s: completed configuration', self.name) _LOGGER.debug('%s: completed configuration', self.name)
async def async_initialize(self, from_cache=False): async def async_initialize(self, from_cache=False):
"""Initialize listeners.""" """Initialize channels."""
_LOGGER.debug('%s: started initialization', self.name) _LOGGER.debug('%s: started initialization', self.name)
await self._execute_listener_tasks('async_initialize', from_cache) await self._execute_channel_tasks('async_initialize', from_cache)
self.power_source = self.cluster_listeners.get( self.power_source = self.cluster_channels.get(
LISTENER_BASIC).get_power_source() BASIC_CHANNEL).get_power_source()
_LOGGER.debug( _LOGGER.debug(
'%s: power source: %s', '%s: power source: %s',
self.name, self.name,
BasicListener.POWER_SOURCES.get(self.power_source) BasicChannel.POWER_SOURCES.get(self.power_source)
) )
_LOGGER.debug('%s: completed initialization', self.name) _LOGGER.debug('%s: completed initialization', self.name)
async def _execute_listener_tasks(self, task_name, *args): async def _execute_channel_tasks(self, task_name, *args):
"""Gather and execute a set of listener tasks.""" """Gather and execute a set of CHANNEL tasks."""
listener_tasks = [] channel_tasks = []
for listener in self.all_listeners: for channel in self.all_channels:
listener_tasks.append( channel_tasks.append(
self._async_create_task(listener, task_name, *args)) self._async_create_task(channel, task_name, *args))
await asyncio.gather(*listener_tasks) await asyncio.gather(*channel_tasks)
async def _async_create_task(self, listener, func_name, *args): async def _async_create_task(self, channel, func_name, *args):
"""Configure a single listener on this device.""" """Configure a single channel on this device."""
try: try:
await getattr(listener, func_name)(*args) await getattr(channel, func_name)(*args)
_LOGGER.debug('%s: listener: %s %s stage succeeded', _LOGGER.debug('%s: channel: %s %s stage succeeded',
self.name, self.name,
"{}-{}".format( "{}-{}".format(
listener.name, listener.unique_id), channel.name, channel.unique_id),
func_name) func_name)
except Exception as ex: # pylint: disable=broad-except except Exception as ex: # pylint: disable=broad-except
_LOGGER.warning( _LOGGER.warning(
'%s listener: %s %s stage failed ex: %s', '%s channel: %s %s stage failed ex: %s',
self.name, self.name,
"{}-{}".format(listener.name, listener.unique_id), "{}-{}".format(channel.name, channel.unique_id),
func_name, func_name,
ex ex
) )

View file

@ -18,15 +18,18 @@ from .const import (
ZHA_DISCOVERY_NEW, DEVICE_CLASS, SINGLE_INPUT_CLUSTER_DEVICE_CLASS, ZHA_DISCOVERY_NEW, DEVICE_CLASS, SINGLE_INPUT_CLUSTER_DEVICE_CLASS,
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, COMPONENT_CLUSTERS, HUMIDITY, SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, COMPONENT_CLUSTERS, HUMIDITY,
TEMPERATURE, ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT, TEMPERATURE, ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT,
GENERIC, SENSOR_TYPE, EVENT_RELAY_CLUSTERS, LISTENER_BATTERY, UNKNOWN, GENERIC, SENSOR_TYPE, EVENT_RELAY_CLUSTERS, UNKNOWN,
OPENING, ZONE, OCCUPANCY, CLUSTER_REPORT_CONFIGS, REPORT_CONFIG_IMMEDIATE, OPENING, ZONE, OCCUPANCY, CLUSTER_REPORT_CONFIGS, REPORT_CONFIG_IMMEDIATE,
REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, REPORT_CONFIG_MIN_INT, REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, REPORT_CONFIG_MIN_INT,
REPORT_CONFIG_MAX_INT, REPORT_CONFIG_OP, SIGNAL_REMOVE, NO_SENSOR_CLUSTERS) REPORT_CONFIG_MAX_INT, REPORT_CONFIG_OP, SIGNAL_REMOVE, NO_SENSOR_CLUSTERS,
POWER_CONFIGURATION_CHANNEL)
from .device import ZHADevice from .device import ZHADevice
from ..device_entity import ZhaDeviceEntity from ..device_entity import ZhaDeviceEntity
from .listeners import ( from .channels import (
LISTENER_REGISTRY, AttributeListener, EventRelayListener, ZDOListener, AttributeListeningChannel, EventRelayChannel, ZDOChannel
BasicListener) )
from .channels.general import BasicChannel
from .channels.registry import ZIGBEE_CHANNEL_REGISTRY
from .helpers import convert_ieee from .helpers import convert_ieee
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -34,7 +37,7 @@ _LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {} SENSOR_TYPES = {}
BINARY_SENSOR_TYPES = {} BINARY_SENSOR_TYPES = {}
EntityReference = collections.namedtuple( EntityReference = collections.namedtuple(
'EntityReference', 'reference_id zha_device cluster_listeners device_info') 'EntityReference', 'reference_id zha_device cluster_channels device_info')
class ZHAGateway: class ZHAGateway:
@ -106,14 +109,14 @@ class ZHAGateway:
return self._device_registry return self._device_registry
def register_entity_reference( def register_entity_reference(
self, ieee, reference_id, zha_device, cluster_listeners, self, ieee, reference_id, zha_device, cluster_channels,
device_info): device_info):
"""Record the creation of a hass entity associated with ieee.""" """Record the creation of a hass entity associated with ieee."""
self._device_registry[ieee].append( self._device_registry[ieee].append(
EntityReference( EntityReference(
reference_id=reference_id, reference_id=reference_id,
zha_device=zha_device, zha_device=zha_device,
cluster_listeners=cluster_listeners, cluster_channels=cluster_channels,
device_info=device_info device_info=device_info
) )
) )
@ -169,14 +172,14 @@ class ZHAGateway:
# available and we already loaded fresh state above # available and we already loaded fresh state above
zha_device.update_available(True) zha_device.update_available(True)
elif not zha_device.available and zha_device.power_source is not None\ elif not zha_device.available and zha_device.power_source is not None\
and zha_device.power_source != BasicListener.BATTERY: and zha_device.power_source != BasicChannel.BATTERY:
# the device is currently marked unavailable and it isn't a battery # the device is currently marked unavailable and it isn't a battery
# powered device so we should be able to update it now # powered device so we should be able to update it now
_LOGGER.debug( _LOGGER.debug(
"attempting to request fresh state for %s %s", "attempting to request fresh state for %s %s",
zha_device.name, zha_device.name,
"with power source: {}".format( "with power source: {}".format(
BasicListener.POWER_SOURCES.get(zha_device.power_source) BasicChannel.POWER_SOURCES.get(zha_device.power_source)
) )
) )
await zha_device.async_initialize(from_cache=False) await zha_device.async_initialize(from_cache=False)
@ -188,11 +191,11 @@ class ZHAGateway:
import zigpy.profiles import zigpy.profiles
if endpoint_id == 0: # ZDO if endpoint_id == 0: # ZDO
await _create_cluster_listener( await _create_cluster_channel(
endpoint, endpoint,
zha_device, zha_device,
is_new_join, is_new_join,
listener_class=ZDOListener channel_class=ZDOChannel
) )
return return
@ -234,18 +237,18 @@ class ZHAGateway:
)) ))
async def _create_cluster_listener(cluster, zha_device, is_new_join, async def _create_cluster_channel(cluster, zha_device, is_new_join,
listeners=None, listener_class=None): channels=None, channel_class=None):
"""Create a cluster listener and attach it to a device.""" """Create a cluster channel and attach it to a device."""
if listener_class is None: if channel_class is None:
listener_class = LISTENER_REGISTRY.get(cluster.cluster_id, channel_class = ZIGBEE_CHANNEL_REGISTRY.get(cluster.cluster_id,
AttributeListener) AttributeListeningChannel)
listener = listener_class(cluster, zha_device) channel = channel_class(cluster, zha_device)
if is_new_join: if is_new_join:
await listener.async_configure() await channel.async_configure()
zha_device.add_cluster_listener(listener) zha_device.add_cluster_channel(channel)
if listeners is not None: if channels is not None:
listeners.append(listener) channels.append(channel)
async def _dispatch_discovery_info(hass, is_new_join, discovery_info): async def _dispatch_discovery_info(hass, is_new_join, discovery_info):
@ -272,23 +275,23 @@ async def _handle_profile_match(hass, endpoint, profile_clusters, zha_device,
for c in profile_clusters[1] for c in profile_clusters[1]
if c in endpoint.out_clusters] if c in endpoint.out_clusters]
listeners = [] channels = []
cluster_tasks = [] cluster_tasks = []
for cluster in in_clusters: for cluster in in_clusters:
cluster_tasks.append(_create_cluster_listener( cluster_tasks.append(_create_cluster_channel(
cluster, zha_device, is_new_join, listeners=listeners)) cluster, zha_device, is_new_join, channels=channels))
for cluster in out_clusters: for cluster in out_clusters:
cluster_tasks.append(_create_cluster_listener( cluster_tasks.append(_create_cluster_channel(
cluster, zha_device, is_new_join, listeners=listeners)) cluster, zha_device, is_new_join, channels=channels))
await asyncio.gather(*cluster_tasks) await asyncio.gather(*cluster_tasks)
discovery_info = { discovery_info = {
'unique_id': device_key, 'unique_id': device_key,
'zha_device': zha_device, 'zha_device': zha_device,
'listeners': listeners, 'channels': channels,
'component': component 'component': component
} }
@ -314,7 +317,7 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device,
"""Dispatch single cluster matches to HA components.""" """Dispatch single cluster matches to HA components."""
cluster_matches = [] cluster_matches = []
cluster_match_tasks = [] cluster_match_tasks = []
event_listener_tasks = [] event_channel_tasks = []
for cluster in endpoint.in_clusters.values(): for cluster in endpoint.in_clusters.values():
if cluster.cluster_id not in profile_clusters[0]: if cluster.cluster_id not in profile_clusters[0]:
cluster_match_tasks.append(_handle_single_cluster_match( cluster_match_tasks.append(_handle_single_cluster_match(
@ -327,7 +330,7 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device,
)) ))
if cluster.cluster_id in NO_SENSOR_CLUSTERS: if cluster.cluster_id in NO_SENSOR_CLUSTERS:
cluster_match_tasks.append(_handle_listener_only_cluster_match( cluster_match_tasks.append(_handle_channel_only_cluster_match(
zha_device, zha_device,
cluster, cluster,
is_new_join, is_new_join,
@ -345,13 +348,13 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device,
)) ))
if cluster.cluster_id in EVENT_RELAY_CLUSTERS: if cluster.cluster_id in EVENT_RELAY_CLUSTERS:
event_listener_tasks.append(_create_cluster_listener( event_channel_tasks.append(_create_cluster_channel(
cluster, cluster,
zha_device, zha_device,
is_new_join, is_new_join,
listener_class=EventRelayListener channel_class=EventRelayChannel
)) ))
await asyncio.gather(*event_listener_tasks) await asyncio.gather(*event_channel_tasks)
cluster_match_results = await asyncio.gather(*cluster_match_tasks) cluster_match_results = await asyncio.gather(*cluster_match_tasks)
for cluster_match in cluster_match_results: for cluster_match in cluster_match_results:
if cluster_match is not None: if cluster_match is not None:
@ -359,10 +362,10 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device,
return cluster_matches return cluster_matches
async def _handle_listener_only_cluster_match( async def _handle_channel_only_cluster_match(
zha_device, cluster, is_new_join): zha_device, cluster, is_new_join):
"""Handle a listener only cluster match.""" """Handle a channel only cluster match."""
await _create_cluster_listener(cluster, zha_device, is_new_join) await _create_cluster_channel(cluster, zha_device, is_new_join)
async def _handle_single_cluster_match(hass, zha_device, cluster, device_key, async def _handle_single_cluster_match(hass, zha_device, cluster, device_key,
@ -376,15 +379,15 @@ async def _handle_single_cluster_match(hass, zha_device, cluster, device_key,
if component is None or component not in COMPONENTS: if component is None or component not in COMPONENTS:
return return
listeners = [] channels = []
await _create_cluster_listener(cluster, zha_device, is_new_join, await _create_cluster_channel(cluster, zha_device, is_new_join,
listeners=listeners) channels=channels)
cluster_key = "{}-{}".format(device_key, cluster.cluster_id) cluster_key = "{}-{}".format(device_key, cluster.cluster_id)
discovery_info = { discovery_info = {
'unique_id': cluster_key, 'unique_id': cluster_key,
'zha_device': zha_device, 'zha_device': zha_device,
'listeners': listeners, 'channels': channels,
'entity_suffix': '_{}'.format(cluster.cluster_id), 'entity_suffix': '_{}'.format(cluster.cluster_id),
'component': component 'component': component
} }
@ -403,11 +406,11 @@ async def _handle_single_cluster_match(hass, zha_device, cluster, device_key,
def _create_device_entity(zha_device): def _create_device_entity(zha_device):
"""Create ZHADeviceEntity.""" """Create ZHADeviceEntity."""
device_entity_listeners = [] device_entity_channels = []
if LISTENER_BATTERY in zha_device.cluster_listeners: if POWER_CONFIGURATION_CHANNEL in zha_device.cluster_channels:
listener = zha_device.cluster_listeners.get(LISTENER_BATTERY) channel = zha_device.cluster_channels.get(POWER_CONFIGURATION_CHANNEL)
device_entity_listeners.append(listener) device_entity_channels.append(channel)
return ZhaDeviceEntity(zha_device, device_entity_listeners) return ZhaDeviceEntity(zha_device, device_entity_channels)
def establish_device_mappings(): def establish_device_mappings():

View file

@ -1,706 +0,0 @@
"""
Cluster listeners for Zigbee Home Automation.
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 homeassistant.helpers.dispatcher import async_dispatcher_send
from .helpers import (
bind_configure_reporting, construct_unique_id,
safe_read, get_attr_id_by_name, bind_cluster)
from .const import (
CLUSTER_REPORT_CONFIGS, REPORT_CONFIG_DEFAULT, SIGNAL_ATTR_UPDATED,
SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL, SIGNAL_STATE_ATTR, LISTENER_BASIC,
LISTENER_ATTRIBUTE, LISTENER_ON_OFF, LISTENER_COLOR, LISTENER_FAN,
LISTENER_LEVEL, LISTENER_ZONE, LISTENER_ACTIVE_POWER, LISTENER_BATTERY,
LISTENER_EVENT_RELAY
)
LISTENER_REGISTRY = {}
_LOGGER = logging.getLogger(__name__)
def populate_listener_registry():
"""Populate the listener registry."""
from zigpy import zcl
LISTENER_REGISTRY.update({
zcl.clusters.general.Alarms.cluster_id: ClusterListener,
zcl.clusters.general.Commissioning.cluster_id: ClusterListener,
zcl.clusters.general.Identify.cluster_id: ClusterListener,
zcl.clusters.general.Groups.cluster_id: ClusterListener,
zcl.clusters.general.Scenes.cluster_id: ClusterListener,
zcl.clusters.general.Partition.cluster_id: ClusterListener,
zcl.clusters.general.Ota.cluster_id: ClusterListener,
zcl.clusters.general.PowerProfile.cluster_id: ClusterListener,
zcl.clusters.general.ApplianceControl.cluster_id: ClusterListener,
zcl.clusters.general.PollControl.cluster_id: ClusterListener,
zcl.clusters.general.GreenPowerProxy.cluster_id: ClusterListener,
zcl.clusters.general.OnOffConfiguration.cluster_id: ClusterListener,
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.general.Basic.cluster_id: BasicListener,
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'",
unique_id,
cmd,
args,
cluster.cluster_id,
tsn
)
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))
if isinstance(result, bool):
return 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
class ClusterListener:
"""Listener for a Zigbee cluster."""
def __init__(self, cluster, device):
"""Initialize ClusterListener."""
self.name = 'cluster_{}'.format(cluster.cluster_id)
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
self._cluster.add_listener(self)
@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
@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 events to hass."""
self._zha_device.hass.bus.async_fire(
'zha_event',
{
'unique_id': self._unique_id,
'device_ieee': str(self._zha_device.ieee),
'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."""
def __init__(self, cluster, device):
"""Initialize AttributeListener."""
super().__init__(cluster, device)
self.name = LISTENER_ATTRIBUTE
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."""
ON_OFF = 0
def __init__(self, cluster, device):
"""Initialize OnOffListener."""
super().__init__(cluster, device)
self.name = LISTENER_ON_OFF
self._state = None
@callback
def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster."""
cmd = parse_and_log_command(
self.unique_id,
self._cluster,
tsn,
command_id,
args
)
if cmd in ('off', 'off_with_effect'):
self.attribute_updated(self.ON_OFF, False)
elif cmd in ('on', 'on_with_recall_global_scene', 'on_with_timed_off'):
self.attribute_updated(self.ON_OFF, True)
elif cmd == 'toggle':
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:
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."""
CURRENT_LEVEL = 0
def __init__(self, cluster, device):
"""Initialize LevelListener."""
super().__init__(cluster, device)
self.name = LISTENER_LEVEL
@callback
def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster."""
cmd = parse_and_log_command(
self.unique_id,
self._cluster,
tsn,
command_id,
args
)
if cmd in ('move_to_level', 'move_to_level_with_on_off'):
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.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.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.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."""
def __init__(self, cluster, device):
"""Initialize LevelListener."""
super().__init__(cluster, device)
self.name = LISTENER_ZONE
@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)
await bind_cluster(self.unique_id, self._cluster)
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)
class ActivePowerListener(AttributeListener):
"""Listener that polls active power level."""
def __init__(self, cluster, device):
"""Initialize ActivePowerListener."""
super().__init__(cluster, device)
self.name = LISTENER_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(
LISTENER_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(
LISTENER_ACTIVE_POWER, from_cache=from_cache)
await super().async_initialize(from_cache)
class BasicListener(ClusterListener):
"""Listener to interact with the basic cluster."""
BATTERY = 3
POWER_SOURCES = {
0: 'Unknown',
1: 'Mains (single phase)',
2: 'Mains (3 phase)',
BATTERY: 'Battery',
4: 'DC source',
5: 'Emergency mains constantly powered',
6: 'Emergency mains and transfer switch'
}
def __init__(self, cluster, device):
"""Initialize BasicListener."""
super().__init__(cluster, device)
self.name = LISTENER_BASIC
self._power_source = None
async def async_configure(self):
"""Configure this listener."""
await super().async_configure()
await self.async_initialize(False)
async def async_initialize(self, from_cache):
"""Initialize listener."""
self._power_source = await self.get_attribute_value(
'power_source', from_cache=from_cache)
await super().async_initialize(from_cache)
def get_power_source(self):
"""Get the power source."""
return self._power_source
class BatteryListener(ClusterListener):
"""Listener that polls active power level."""
def __init__(self, cluster, device):
"""Initialize BatteryListener."""
super().__init__(cluster, device)
self.name = LISTENER_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):
"""Event relay that can be attached to zigbee clusters."""
def __init__(self, cluster, device):
"""Initialize EventRelayListener."""
super().__init__(cluster, device)
self.name = LISTENER_EVENT_RELAY
@callback
def attribute_updated(self, attrid, value):
"""Handle an attribute updated on this cluster."""
self.zha_send_event(
self._cluster,
SIGNAL_ATTR_UPDATED,
{
'attribute_id': attrid,
'attribute_name': self._cluster.attributes.get(
attrid,
['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
)
class ColorListener(ClusterListener):
"""Color listener."""
CAPABILITIES_COLOR_XY = 0x08
CAPABILITIES_COLOR_TEMP = 0x10
UNSUPPORTED_ATTRIBUTE = 0x86
def __init__(self, cluster, device):
"""Initialize ColorListener."""
super().__init__(cluster, device)
self.name = LISTENER_COLOR
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."""
_value_attribute = 0
def __init__(self, cluster, device):
"""Initialize FanListener."""
super().__init__(cluster, device)
self.name = LISTENER_FAN
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."""
def __init__(self, cluster, device):
"""Initialize ZDOListener."""
self.name = 'zdo'
self._cluster = cluster
self._zha_device = device
self._status = ListenerStatus.CREATED
self._unique_id = "{}_ZDO".format(device.name)
self._cluster.add_listener(self)
@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 async_initialize(self, from_cache):
"""Initialize listener."""
self._status = ListenerStatus.INITIALIZED
async def async_configure(self):
"""Configure listener."""
self._status = ListenerStatus.CONFIGURED

View file

@ -11,7 +11,7 @@ import time
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.util import slugify from homeassistant.util import slugify
from .entity import ZhaEntity from .entity import ZhaEntity
from .const import LISTENER_BATTERY, SIGNAL_STATE_ATTR from .const import POWER_CONFIGURATION_CHANNEL, SIGNAL_STATE_ATTR
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -38,7 +38,7 @@ STATE_OFFLINE = 'offline'
class ZhaDeviceEntity(ZhaEntity): class ZhaDeviceEntity(ZhaEntity):
"""A base class for ZHA devices.""" """A base class for ZHA devices."""
def __init__(self, zha_device, listeners, keepalive_interval=7200, def __init__(self, zha_device, channels, keepalive_interval=7200,
**kwargs): **kwargs):
"""Init ZHA endpoint entity.""" """Init ZHA endpoint entity."""
ieee = zha_device.ieee ieee = zha_device.ieee
@ -55,7 +55,7 @@ class ZhaDeviceEntity(ZhaEntity):
unique_id = str(ieeetail) unique_id = str(ieeetail)
kwargs['component'] = 'zha' kwargs['component'] = 'zha'
super().__init__(unique_id, zha_device, listeners, skip_entity_id=True, super().__init__(unique_id, zha_device, channels, skip_entity_id=True,
**kwargs) **kwargs)
self._keepalive_interval = keepalive_interval self._keepalive_interval = keepalive_interval
@ -66,7 +66,8 @@ class ZhaDeviceEntity(ZhaEntity):
'rssi': zha_device.rssi, 'rssi': zha_device.rssi,
}) })
self._should_poll = True self._should_poll = True
self._battery_listener = self.cluster_listeners.get(LISTENER_BATTERY) self._battery_channel = self.cluster_channels.get(
POWER_CONFIGURATION_CHANNEL)
@property @property
def state(self) -> str: def state(self) -> str:
@ -97,9 +98,9 @@ class ZhaDeviceEntity(ZhaEntity):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Run when about to be added to hass.""" """Run when about to be added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
if self._battery_listener: if self._battery_channel:
await self.async_accept_signal( await self.async_accept_signal(
self._battery_listener, SIGNAL_STATE_ATTR, self._battery_channel, SIGNAL_STATE_ATTR,
self.async_update_state_attribute) self.async_update_state_attribute)
# only do this on add to HA because it is static # only do this on add to HA because it is static
await self._async_init_battery_values() await self._async_init_battery_values()
@ -114,7 +115,7 @@ class ZhaDeviceEntity(ZhaEntity):
self._zha_device.update_available(False) self._zha_device.update_available(False)
else: else:
self._zha_device.update_available(True) self._zha_device.update_available(True)
if self._battery_listener: if self._battery_channel:
await self.async_get_latest_battery_reading() await self.async_get_latest_battery_reading()
@callback @callback
@ -127,14 +128,14 @@ class ZhaDeviceEntity(ZhaEntity):
super().async_set_available(available) super().async_set_available(available)
async def _async_init_battery_values(self): async def _async_init_battery_values(self):
"""Get initial battery level and battery info from listener cache.""" """Get initial battery level and battery info from channel cache."""
battery_size = await self._battery_listener.get_attribute_value( battery_size = await self._battery_channel.get_attribute_value(
'battery_size') 'battery_size')
if battery_size is not None: if battery_size is not None:
self._device_state_attributes['battery_size'] = BATTERY_SIZES.get( self._device_state_attributes['battery_size'] = BATTERY_SIZES.get(
battery_size, 'Unknown') battery_size, 'Unknown')
battery_quantity = await self._battery_listener.get_attribute_value( battery_quantity = await self._battery_channel.get_attribute_value(
'battery_quantity') 'battery_quantity')
if battery_quantity is not None: if battery_quantity is not None:
self._device_state_attributes['battery_quantity'] = \ self._device_state_attributes['battery_quantity'] = \
@ -142,8 +143,8 @@ class ZhaDeviceEntity(ZhaEntity):
await self.async_get_latest_battery_reading() await self.async_get_latest_battery_reading()
async def async_get_latest_battery_reading(self): async def async_get_latest_battery_reading(self):
"""Get the latest battery reading from listeners cache.""" """Get the latest battery reading from channels cache."""
battery = await self._battery_listener.get_attribute_value( battery = await self._battery_channel.get_attribute_value(
'battery_percentage_remaining') 'battery_percentage_remaining')
if battery is not None: if battery is not None:
self._device_state_attributes['battery_level'] = battery self._device_state_attributes['battery_level'] = battery

View file

@ -27,7 +27,7 @@ class ZhaEntity(entity.Entity):
_domain = None # Must be overridden by subclasses _domain = None # Must be overridden by subclasses
def __init__(self, unique_id, zha_device, listeners, def __init__(self, unique_id, zha_device, channels,
skip_entity_id=False, **kwargs): skip_entity_id=False, **kwargs):
"""Init ZHA entity.""" """Init ZHA entity."""
self._force_update = False self._force_update = False
@ -48,25 +48,25 @@ class ZhaEntity(entity.Entity):
slugify(zha_device.manufacturer), slugify(zha_device.manufacturer),
slugify(zha_device.model), slugify(zha_device.model),
ieeetail, ieeetail,
listeners[0].cluster.endpoint.endpoint_id, channels[0].cluster.endpoint.endpoint_id,
kwargs.get(ENTITY_SUFFIX, ''), kwargs.get(ENTITY_SUFFIX, ''),
) )
else: else:
self.entity_id = "{}.zha_{}_{}{}".format( self.entity_id = "{}.zha_{}_{}{}".format(
self._domain, self._domain,
ieeetail, ieeetail,
listeners[0].cluster.endpoint.endpoint_id, channels[0].cluster.endpoint.endpoint_id,
kwargs.get(ENTITY_SUFFIX, ''), kwargs.get(ENTITY_SUFFIX, ''),
) )
self._state = None self._state = None
self._device_state_attributes = {} self._device_state_attributes = {}
self._zha_device = zha_device self._zha_device = zha_device
self.cluster_listeners = {} self.cluster_channels = {}
self._available = False self._available = False
self._component = kwargs['component'] self._component = kwargs['component']
self._unsubs = [] self._unsubs = []
for listener in listeners: for channel in channels:
self.cluster_listeners[listener.name] = listener self.cluster_channels[channel.name] = channel
@property @property
def name(self): def name(self):
@ -147,7 +147,7 @@ class ZhaEntity(entity.Entity):
) )
self._zha_device.gateway.register_entity_reference( self._zha_device.gateway.register_entity_reference(
self._zha_device.ieee, self.entity_id, self._zha_device, self._zha_device.ieee, self.entity_id, self._zha_device,
self.cluster_listeners, self.device_info) self.cluster_channels, self.device_info)
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Disconnect entity object when removed.""" """Disconnect entity object when removed."""
@ -156,13 +156,13 @@ class ZhaEntity(entity.Entity):
async def async_update(self): async def async_update(self):
"""Retrieve latest state.""" """Retrieve latest state."""
for listener in self.cluster_listeners: for channel in self.cluster_channels:
if hasattr(listener, 'async_update'): if hasattr(channel, 'async_update'):
await listener.async_update() await channel.async_update()
async def async_accept_signal(self, listener, signal, func, async def async_accept_signal(self, channel, signal, func,
signal_override=False): signal_override=False):
"""Accept a signal from a listener.""" """Accept a signal from a channel."""
unsub = None unsub = None
if signal_override: if signal_override:
unsub = async_dispatcher_connect( unsub = async_dispatcher_connect(
@ -173,7 +173,7 @@ class ZhaEntity(entity.Entity):
else: else:
unsub = async_dispatcher_connect( unsub = async_dispatcher_connect(
self.hass, self.hass,
"{}_{}".format(listener.unique_id, signal), "{}_{}".format(channel.unique_id, signal),
func func
) )
self._unsubs.append(unsub) self._unsubs.append(unsub)

View file

@ -11,7 +11,7 @@ from homeassistant.components.fan import (
FanEntity) FanEntity)
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .core.const import ( from .core.const import (
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, LISTENER_FAN, DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, FAN_CHANNEL,
SIGNAL_ATTR_UPDATED SIGNAL_ATTR_UPDATED
) )
from .entity import ZhaEntity from .entity import ZhaEntity
@ -81,16 +81,16 @@ class ZhaFan(ZhaEntity, FanEntity):
_domain = DOMAIN _domain = DOMAIN
def __init__(self, unique_id, zha_device, listeners, **kwargs): def __init__(self, unique_id, zha_device, channels, **kwargs):
"""Init this sensor.""" """Init this sensor."""
super().__init__(unique_id, zha_device, listeners, **kwargs) super().__init__(unique_id, zha_device, channels, **kwargs)
self._fan_listener = self.cluster_listeners.get(LISTENER_FAN) self._fan_channel = self.cluster_channels.get(FAN_CHANNEL)
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Run when about to be added to hass.""" """Run when about to be added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
await self.async_accept_signal( await self.async_accept_signal(
self._fan_listener, SIGNAL_ATTR_UPDATED, self.async_set_state) self._fan_channel, SIGNAL_ATTR_UPDATED, self.async_set_state)
@property @property
def supported_features(self) -> int: def supported_features(self) -> int:
@ -120,7 +120,7 @@ class ZhaFan(ZhaEntity, FanEntity):
return self.state_attributes return self.state_attributes
def async_set_state(self, state): def async_set_state(self, state):
"""Handle state update from listener.""" """Handle state update from channel."""
self._state = VALUE_TO_SPEED.get(state, self._state) self._state = VALUE_TO_SPEED.get(state, self._state)
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
@ -137,5 +137,5 @@ class ZhaFan(ZhaEntity, FanEntity):
async def async_set_speed(self, speed: str) -> None: async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan.""" """Set the speed of the fan."""
await self._fan_listener.async_set_speed(SPEED_TO_VALUE[speed]) await self._fan_channel.async_set_speed(SPEED_TO_VALUE[speed])
self.async_set_state(speed) self.async_set_state(speed)

View file

@ -10,8 +10,8 @@ from homeassistant.components import light
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from .const import ( from .const import (
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, LISTENER_COLOR, DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, COLOR_CHANNEL,
LISTENER_ON_OFF, LISTENER_LEVEL, SIGNAL_ATTR_UPDATED, SIGNAL_SET_LEVEL ON_OFF_CHANNEL, LEVEL_CHANNEL, SIGNAL_ATTR_UPDATED, SIGNAL_SET_LEVEL
) )
from .entity import ZhaEntity from .entity import ZhaEntity
@ -67,24 +67,24 @@ class Light(ZhaEntity, light.Light):
_domain = light.DOMAIN _domain = light.DOMAIN
def __init__(self, unique_id, zha_device, listeners, **kwargs): def __init__(self, unique_id, zha_device, channels, **kwargs):
"""Initialize the ZHA light.""" """Initialize the ZHA light."""
super().__init__(unique_id, zha_device, listeners, **kwargs) super().__init__(unique_id, zha_device, channels, **kwargs)
self._supported_features = 0 self._supported_features = 0
self._color_temp = None self._color_temp = None
self._hs_color = None self._hs_color = None
self._brightness = None self._brightness = None
self._on_off_listener = self.cluster_listeners.get(LISTENER_ON_OFF) self._on_off_channel = self.cluster_channels.get(ON_OFF_CHANNEL)
self._level_listener = self.cluster_listeners.get(LISTENER_LEVEL) self._level_channel = self.cluster_channels.get(LEVEL_CHANNEL)
self._color_listener = self.cluster_listeners.get(LISTENER_COLOR) self._color_channel = self.cluster_channels.get(COLOR_CHANNEL)
if self._level_listener: if self._level_channel:
self._supported_features |= light.SUPPORT_BRIGHTNESS self._supported_features |= light.SUPPORT_BRIGHTNESS
self._supported_features |= light.SUPPORT_TRANSITION self._supported_features |= light.SUPPORT_TRANSITION
self._brightness = 0 self._brightness = 0
if self._color_listener: if self._color_channel:
color_capabilities = self._color_listener.get_color_capabilities() color_capabilities = self._color_channel.get_color_capabilities()
if color_capabilities & CAPABILITIES_COLOR_TEMP: if color_capabilities & CAPABILITIES_COLOR_TEMP:
self._supported_features |= light.SUPPORT_COLOR_TEMP self._supported_features |= light.SUPPORT_COLOR_TEMP
@ -139,10 +139,10 @@ class Light(ZhaEntity, light.Light):
"""Run when about to be added to hass.""" """Run when about to be added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
await self.async_accept_signal( await self.async_accept_signal(
self._on_off_listener, SIGNAL_ATTR_UPDATED, self.async_set_state) self._on_off_channel, SIGNAL_ATTR_UPDATED, self.async_set_state)
if self._level_listener: if self._level_channel:
await self.async_accept_signal( await self.async_accept_signal(
self._level_listener, SIGNAL_SET_LEVEL, self.set_level) self._level_channel, SIGNAL_SET_LEVEL, self.set_level)
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn the entity on.""" """Turn the entity on."""
@ -152,7 +152,7 @@ class Light(ZhaEntity, light.Light):
if light.ATTR_COLOR_TEMP in kwargs and \ if light.ATTR_COLOR_TEMP in kwargs and \
self.supported_features & light.SUPPORT_COLOR_TEMP: self.supported_features & light.SUPPORT_COLOR_TEMP:
temperature = kwargs[light.ATTR_COLOR_TEMP] temperature = kwargs[light.ATTR_COLOR_TEMP]
success = await self._color_listener.move_to_color_temp( success = await self._color_channel.move_to_color_temp(
temperature, duration) temperature, duration)
if not success: if not success:
return return
@ -162,7 +162,7 @@ class Light(ZhaEntity, light.Light):
self.supported_features & light.SUPPORT_COLOR: self.supported_features & light.SUPPORT_COLOR:
hs_color = kwargs[light.ATTR_HS_COLOR] hs_color = kwargs[light.ATTR_HS_COLOR]
xy_color = color_util.color_hs_to_xy(*hs_color) xy_color = color_util.color_hs_to_xy(*hs_color)
success = await self._color_listener.move_to_color( success = await self._color_channel.move_to_color(
int(xy_color[0] * 65535), int(xy_color[0] * 65535),
int(xy_color[1] * 65535), int(xy_color[1] * 65535),
duration, duration,
@ -174,7 +174,7 @@ class Light(ZhaEntity, light.Light):
if self._brightness is not None: if self._brightness is not None:
brightness = kwargs.get( brightness = kwargs.get(
light.ATTR_BRIGHTNESS, self._brightness or 255) light.ATTR_BRIGHTNESS, self._brightness or 255)
success = await self._level_listener.move_to_level_with_on_off( success = await self._level_channel.move_to_level_with_on_off(
brightness, brightness,
duration duration
) )
@ -185,7 +185,7 @@ class Light(ZhaEntity, light.Light):
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
return return
success = await self._on_off_listener.on() success = await self._on_off_channel.on()
if not success: if not success:
return return
@ -198,12 +198,12 @@ class Light(ZhaEntity, light.Light):
supports_level = self.supported_features & light.SUPPORT_BRIGHTNESS supports_level = self.supported_features & light.SUPPORT_BRIGHTNESS
success = None success = None
if duration and supports_level: if duration and supports_level:
success = await self._level_listener.move_to_level_with_on_off( success = await self._level_channel.move_to_level_with_on_off(
0, 0,
duration*10 duration*10
) )
else: else:
success = await self._on_off_listener.off() success = await self._on_off_channel.off()
_LOGGER.debug("%s was turned off: %s", self.entity_id, success) _LOGGER.debug("%s was turned off: %s", self.entity_id, success)
if not success: if not success:
return return

View file

@ -12,7 +12,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .core.const import ( from .core.const import (
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, HUMIDITY, TEMPERATURE, DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, HUMIDITY, TEMPERATURE,
ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT, ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT,
GENERIC, SENSOR_TYPE, LISTENER_ATTRIBUTE, LISTENER_ACTIVE_POWER, GENERIC, SENSOR_TYPE, ATTRIBUTE_CHANNEL, ELECTRICAL_MEASUREMENT_CHANNEL,
SIGNAL_ATTR_UPDATED, SIGNAL_STATE_ATTR) SIGNAL_ATTR_UPDATED, SIGNAL_STATE_ATTR)
from .entity import ZhaEntity from .entity import ZhaEntity
@ -74,8 +74,8 @@ UNIT_REGISTRY = {
GENERIC: None GENERIC: None
} }
LISTENER_REGISTRY = { CHANNEL_REGISTRY = {
ELECTRICAL_MEASUREMENT: LISTENER_ACTIVE_POWER, ELECTRICAL_MEASUREMENT: ELECTRICAL_MEASUREMENT_CHANNEL,
} }
POLLING_REGISTRY = { POLLING_REGISTRY = {
@ -130,9 +130,9 @@ class Sensor(ZhaEntity):
_domain = DOMAIN _domain = DOMAIN
def __init__(self, unique_id, zha_device, listeners, **kwargs): def __init__(self, unique_id, zha_device, channels, **kwargs):
"""Init this sensor.""" """Init this sensor."""
super().__init__(unique_id, zha_device, listeners, **kwargs) super().__init__(unique_id, zha_device, channels, **kwargs)
sensor_type = kwargs.get(SENSOR_TYPE, GENERIC) sensor_type = kwargs.get(SENSOR_TYPE, GENERIC)
self._unit = UNIT_REGISTRY.get(sensor_type) self._unit = UNIT_REGISTRY.get(sensor_type)
self._formatter_function = FORMATTER_FUNC_REGISTRY.get( self._formatter_function = FORMATTER_FUNC_REGISTRY.get(
@ -147,17 +147,17 @@ class Sensor(ZhaEntity):
sensor_type, sensor_type,
False False
) )
self._listener = self.cluster_listeners.get( self._channel = self.cluster_channels.get(
LISTENER_REGISTRY.get(sensor_type, LISTENER_ATTRIBUTE) CHANNEL_REGISTRY.get(sensor_type, ATTRIBUTE_CHANNEL)
) )
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Run when about to be added to hass.""" """Run when about to be added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
await self.async_accept_signal( await self.async_accept_signal(
self._listener, SIGNAL_ATTR_UPDATED, self.async_set_state) self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state)
await self.async_accept_signal( await self.async_accept_signal(
self._listener, SIGNAL_STATE_ATTR, self._channel, SIGNAL_STATE_ATTR,
self.async_update_state_attribute) self.async_update_state_attribute)
@property @property
@ -175,6 +175,6 @@ class Sensor(ZhaEntity):
return self._state return self._state
def async_set_state(self, state): def async_set_state(self, state):
"""Handle state update from listener.""" """Handle state update from channel."""
self._state = self._formatter_function(state) self._state = self._formatter_function(state)
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()

View file

@ -9,7 +9,7 @@ import logging
from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.components.switch import DOMAIN, SwitchDevice
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .core.const import ( from .core.const import (
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, LISTENER_ON_OFF, DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, ON_OFF_CHANNEL,
SIGNAL_ATTR_UPDATED SIGNAL_ATTR_UPDATED
) )
from .entity import ZhaEntity from .entity import ZhaEntity
@ -60,7 +60,7 @@ class Switch(ZhaEntity, SwitchDevice):
def __init__(self, **kwargs): def __init__(self, **kwargs):
"""Initialize the ZHA switch.""" """Initialize the ZHA switch."""
super().__init__(**kwargs) super().__init__(**kwargs)
self._on_off_listener = self.cluster_listeners.get(LISTENER_ON_OFF) self._on_off_channel = self.cluster_channels.get(ON_OFF_CHANNEL)
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
@ -71,14 +71,14 @@ class Switch(ZhaEntity, SwitchDevice):
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn the entity on.""" """Turn the entity on."""
await self._on_off_listener.on() await self._on_off_channel.on()
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs):
"""Turn the entity off.""" """Turn the entity off."""
await self._on_off_listener.off() await self._on_off_channel.off()
def async_set_state(self, state): def async_set_state(self, state):
"""Handle state update from listener.""" """Handle state update from channel."""
self._state = bool(state) self._state = bool(state)
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
@ -91,4 +91,4 @@ class Switch(ZhaEntity, SwitchDevice):
"""Run when about to be added to hass.""" """Run when about to be added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
await self.async_accept_signal( await self.async_accept_signal(
self._on_off_listener, SIGNAL_ATTR_UPDATED, self.async_set_state) self._on_off_channel, SIGNAL_ATTR_UPDATED, self.async_set_state)

View file

@ -7,8 +7,8 @@ from homeassistant.components.zha.core.const import (
) )
from homeassistant.components.zha.core.gateway import ZHAGateway from homeassistant.components.zha.core.gateway import ZHAGateway
from homeassistant.components.zha.core.gateway import establish_device_mappings from homeassistant.components.zha.core.gateway import establish_device_mappings
from homeassistant.components.zha.core.listeners \ from homeassistant.components.zha.core.channels.registry \
import populate_listener_registry import populate_channel_registry
from .common import async_setup_entry from .common import async_setup_entry
@ -28,7 +28,7 @@ def zha_gateway_fixture(hass):
Create a ZHAGateway object that can be used to interact with as if we Create a ZHAGateway object that can be used to interact with as if we
had a real zigbee network running. had a real zigbee network running.
""" """
populate_listener_registry() populate_channel_registry()
establish_device_mappings() establish_device_mappings()
for component in COMPONENTS: for component in COMPONENTS:
hass.data[DATA_ZHA][component] = ( hass.data[DATA_ZHA][component] = (