From 3be81780356d2514a8aa3651abd0a78d1cefb2d2 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 19 Feb 2019 12:58:22 -0500 Subject: [PATCH] Refactor ZHA listeners into channels (#21196) * refactor listeners to channels * update coveragerc --- .coveragerc | 2 +- homeassistant/components/zha/__init__.py | 4 +- homeassistant/components/zha/binary_sensor.py | 46 +- homeassistant/components/zha/core/__init__.py | 3 - .../components/zha/core/channels/__init__.py | 308 ++++++++ .../components/zha/core/channels/closures.py | 9 + .../components/zha/core/channels/general.py | 202 +++++ .../zha/core/channels/homeautomation.py | 40 + .../components/zha/core/channels/hvac.py | 62 ++ .../components/zha/core/channels/lighting.py | 48 ++ .../components/zha/core/channels/lightlink.py | 9 + .../zha/core/channels/manufacturerspecific.py | 9 + .../zha/core/channels/measurement.py | 9 + .../components/zha/core/channels/protocol.py | 9 + .../components/zha/core/channels/registry.py | 46 ++ .../components/zha/core/channels/security.py | 82 ++ .../zha/core/channels/smartenergy.py | 9 + homeassistant/components/zha/core/const.py | 20 +- homeassistant/components/zha/core/device.py | 77 +- homeassistant/components/zha/core/gateway.py | 95 +-- .../components/zha/core/listeners.py | 706 ------------------ homeassistant/components/zha/device_entity.py | 25 +- homeassistant/components/zha/entity.py | 26 +- homeassistant/components/zha/fan.py | 14 +- homeassistant/components/zha/light.py | 38 +- homeassistant/components/zha/sensor.py | 20 +- homeassistant/components/zha/switch.py | 12 +- tests/components/zha/conftest.py | 6 +- 28 files changed, 1037 insertions(+), 899 deletions(-) create mode 100644 homeassistant/components/zha/core/channels/__init__.py create mode 100644 homeassistant/components/zha/core/channels/closures.py create mode 100644 homeassistant/components/zha/core/channels/general.py create mode 100644 homeassistant/components/zha/core/channels/homeautomation.py create mode 100644 homeassistant/components/zha/core/channels/hvac.py create mode 100644 homeassistant/components/zha/core/channels/lighting.py create mode 100644 homeassistant/components/zha/core/channels/lightlink.py create mode 100644 homeassistant/components/zha/core/channels/manufacturerspecific.py create mode 100644 homeassistant/components/zha/core/channels/measurement.py create mode 100644 homeassistant/components/zha/core/channels/protocol.py create mode 100644 homeassistant/components/zha/core/channels/registry.py create mode 100644 homeassistant/components/zha/core/channels/security.py create mode 100644 homeassistant/components/zha/core/channels/smartenergy.py delete mode 100644 homeassistant/components/zha/core/listeners.py diff --git a/.coveragerc b/.coveragerc index 281fcc3ec19..df2e0df2aed 100644 --- a/.coveragerc +++ b/.coveragerc @@ -669,11 +669,11 @@ omit = homeassistant/components/zha/__init__.py homeassistant/components/zha/api.py homeassistant/components/zha/const.py + homeassistant/components/zha/core/channels/* homeassistant/components/zha/core/const.py homeassistant/components/zha/core/device.py homeassistant/components/zha/core/gateway.py homeassistant/components/zha/core/helpers.py - homeassistant/components/zha/core/listeners.py homeassistant/components/zha/device_entity.py homeassistant/components/zha/entity.py homeassistant/components/zha/light.py diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index b8ef5c40838..6c7e83689ad 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -26,7 +26,7 @@ from .core.const import ( DATA_ZHA_RADIO, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME, 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 +from .core.channels.registry import populate_channel_registry REQUIREMENTS = [ '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. """ establish_device_mappings() - populate_listener_registry() + populate_channel_registry() for component in COMPONENTS: hass.data[DATA_ZHA][component] = ( diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 1f85373eecc..a46ffdd305d 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -9,9 +9,9 @@ import logging from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core.const import ( - 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, + DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, ON_OFF_CHANNEL, + LEVEL_CHANNEL, ZONE_CHANNEL, SIGNAL_ATTR_UPDATED, SIGNAL_MOVE_LEVEL, + SIGNAL_SET_LEVEL, ATTRIBUTE_CHANNEL, UNKNOWN, OPENING, ZONE, OCCUPANCY, ATTR_LEVEL, SENSOR_TYPE) from .entity import ZhaEntity @@ -30,9 +30,9 @@ CLASS_MAPPING = { } -async def get_ias_device_class(listener): - """Get the HA device class from the listener.""" - zone_type = await listener.get_attribute_value('zone_type') +async def get_ias_device_class(channel): + """Get the HA device class from the channel.""" + zone_type = await channel.get_attribute_value('zone_type') return CLASS_MAPPING.get(zone_type) @@ -87,10 +87,10 @@ class BinarySensor(ZhaEntity, BinarySensorDevice): """Initialize the ZHA binary sensor.""" super().__init__(**kwargs) 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._zone_channel = self.cluster_channels.get(ZONE_CHANNEL) + self._on_off_channel = self.cluster_channels.get(ON_OFF_CHANNEL) + self._level_channel = self.cluster_channels.get(LEVEL_CHANNEL) + self._attr_channel = self.cluster_channels.get(ATTRIBUTE_CHANNEL) self._zha_sensor_type = kwargs[SENSOR_TYPE] self._level = None @@ -99,31 +99,31 @@ class BinarySensor(ZhaEntity, BinarySensorDevice): 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: + channel = self.cluster_channels.get(self._zha_sensor_type) + if channel is None: return None - return await device_class_supplier(listener) + return await device_class_supplier(channel) 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() - if self._level_listener: + if self._level_channel: 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( - self._level_listener, SIGNAL_MOVE_LEVEL, self.move_level) - if self._on_off_listener: + self._level_channel, SIGNAL_MOVE_LEVEL, self.move_level) + if self._on_off_channel: await self.async_accept_signal( - self._on_off_listener, SIGNAL_ATTR_UPDATED, + self._on_off_channel, SIGNAL_ATTR_UPDATED, self.async_set_state) - if self._zone_listener: + if self._zone_channel: await self.async_accept_signal( - self._zone_listener, SIGNAL_ATTR_UPDATED, self.async_set_state) - if self._attr_listener: + self._zone_channel, SIGNAL_ATTR_UPDATED, self.async_set_state) + if self._attr_channel: 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 def is_on(self) -> bool: @@ -160,7 +160,7 @@ class BinarySensor(ZhaEntity, BinarySensorDevice): @property def device_state_attributes(self): """Return the device state attributes.""" - if self._level_listener is not None: + if self._level_channel is not None: self._device_state_attributes.update({ ATTR_LEVEL: self._state and self._level or 0 }) diff --git a/homeassistant/components/zha/core/__init__.py b/homeassistant/components/zha/core/__init__.py index e7443e7e0b7..145b725fc79 100644 --- a/homeassistant/components/zha/core/__init__.py +++ b/homeassistant/components/zha/core/__init__.py @@ -8,6 +8,3 @@ 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) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py new file mode 100644 index 00000000000..0c0e1ed2173 --- /dev/null +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -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 + ) diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py new file mode 100644 index 00000000000..ba3b6b2e716 --- /dev/null +++ b/homeassistant/components/zha/core/channels/closures.py @@ -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__) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py new file mode 100644 index 00000000000..bc015ae47f0 --- /dev/null +++ b/homeassistant/components/zha/core/channels/general.py @@ -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) diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py new file mode 100644 index 00000000000..2518889fcb1 --- /dev/null +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -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) diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py new file mode 100644 index 00000000000..c62ec66588e --- /dev/null +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -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) diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py new file mode 100644 index 00000000000..ee88a30e828 --- /dev/null +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -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) diff --git a/homeassistant/components/zha/core/channels/lightlink.py b/homeassistant/components/zha/core/channels/lightlink.py new file mode 100644 index 00000000000..83fca6e80c2 --- /dev/null +++ b/homeassistant/components/zha/core/channels/lightlink.py @@ -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__) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py new file mode 100644 index 00000000000..a0eebd78343 --- /dev/null +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -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__) diff --git a/homeassistant/components/zha/core/channels/measurement.py b/homeassistant/components/zha/core/channels/measurement.py new file mode 100644 index 00000000000..51146289e69 --- /dev/null +++ b/homeassistant/components/zha/core/channels/measurement.py @@ -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__) diff --git a/homeassistant/components/zha/core/channels/protocol.py b/homeassistant/components/zha/core/channels/protocol.py new file mode 100644 index 00000000000..2cae156aec5 --- /dev/null +++ b/homeassistant/components/zha/core/channels/protocol.py @@ -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__) diff --git a/homeassistant/components/zha/core/channels/registry.py b/homeassistant/components/zha/core/channels/registry.py new file mode 100644 index 00000000000..f0363ac8330 --- /dev/null +++ b/homeassistant/components/zha/core/channels/registry.py @@ -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, + }) diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py new file mode 100644 index 00000000000..e8c0e71a263 --- /dev/null +++ b/homeassistant/components/zha/core/channels/security.py @@ -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) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py new file mode 100644 index 00000000000..d17eae30a96 --- /dev/null +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -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__) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index faa423d8ac4..d1001682c7b 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -70,16 +70,16 @@ OCCUPANCY = 'occupancy' ATTR_LEVEL = 'level' -LISTENER_ON_OFF = 'on_off' -LISTENER_ATTRIBUTE = 'attribute' -LISTENER_BASIC = 'basic' -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' +ON_OFF_CHANNEL = 'on_off' +ATTRIBUTE_CHANNEL = 'attribute' +BASIC_CHANNEL = 'basic' +COLOR_CHANNEL = 'color' +FAN_CHANNEL = 'fan' +LEVEL_CHANNEL = ATTR_LEVEL +ZONE_CHANNEL = 'zone' +ELECTRICAL_MEASUREMENT_CHANNEL = 'active_power' +POWER_CONFIGURATION_CHANNEL = 'battery' +EVENT_RELAY_CHANNEL = 'event_relay' SIGNAL_ATTR_UPDATED = 'attribute_updated' SIGNAL_MOVE_LEVEL = "move_level" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 7bb39f943f6..3a012ed7895 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -11,13 +11,14 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send ) 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_COMMAND_TYPE, ATTR_ARGS, CLIENT_COMMANDS, SERVER_COMMANDS, 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__) @@ -38,9 +39,9 @@ class ZHADevice: 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._relay_listeners = [] - self._all_listeners = [] + self.cluster_channels = {} + self._relay_channels = [] + self._all_channels = [] self._name = "{} {}".format( self.manufacturer, self.model @@ -113,9 +114,9 @@ class ZHADevice: return self._zha_gateway @property - def all_listeners(self): - """Return cluster listeners and relay listeners for device.""" - return self._all_listeners + def all_channels(self): + """Return cluster channels and relay channels for device.""" + return self._all_channels @property def available_signal(self): @@ -156,59 +157,59 @@ class ZHADevice: QUIRK_CLASS: self.quirk_class } - def add_cluster_listener(self, cluster_listener): - """Add cluster listener to device.""" - # only keep 1 power listener - if cluster_listener.name is LISTENER_BATTERY and \ - LISTENER_BATTERY in self.cluster_listeners: + def add_cluster_channel(self, cluster_channel): + """Add cluster channel to device.""" + # only keep 1 power configuration channel + if cluster_channel.name is POWER_CONFIGURATION_CHANNEL and \ + POWER_CONFIGURATION_CHANNEL in self.cluster_channels: return - self._all_listeners.append(cluster_listener) - if isinstance(cluster_listener, EventRelayListener): - self._relay_listeners.append(cluster_listener) + self._all_channels.append(cluster_channel) + if isinstance(cluster_channel, EventRelayChannel): + self._relay_channels.append(cluster_channel) else: - self.cluster_listeners[cluster_listener.name] = cluster_listener + self.cluster_channels[cluster_channel.name] = cluster_channel async def async_configure(self): """Configure the device.""" _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) async def async_initialize(self, from_cache=False): - """Initialize listeners.""" + """Initialize channels.""" _LOGGER.debug('%s: started initialization', self.name) - await self._execute_listener_tasks('async_initialize', from_cache) - self.power_source = self.cluster_listeners.get( - LISTENER_BASIC).get_power_source() + await self._execute_channel_tasks('async_initialize', from_cache) + self.power_source = self.cluster_channels.get( + BASIC_CHANNEL).get_power_source() _LOGGER.debug( '%s: power source: %s', self.name, - BasicListener.POWER_SOURCES.get(self.power_source) + BasicChannel.POWER_SOURCES.get(self.power_source) ) _LOGGER.debug('%s: completed initialization', self.name) - async def _execute_listener_tasks(self, task_name, *args): - """Gather and execute a set of listener tasks.""" - listener_tasks = [] - for listener in self.all_listeners: - listener_tasks.append( - self._async_create_task(listener, task_name, *args)) - await asyncio.gather(*listener_tasks) + async def _execute_channel_tasks(self, task_name, *args): + """Gather and execute a set of CHANNEL tasks.""" + channel_tasks = [] + for channel in self.all_channels: + channel_tasks.append( + self._async_create_task(channel, task_name, *args)) + await asyncio.gather(*channel_tasks) - async def _async_create_task(self, listener, func_name, *args): - """Configure a single listener on this device.""" + async def _async_create_task(self, channel, func_name, *args): + """Configure a single channel on this device.""" try: - await getattr(listener, func_name)(*args) - _LOGGER.debug('%s: listener: %s %s stage succeeded', + await getattr(channel, func_name)(*args) + _LOGGER.debug('%s: channel: %s %s stage succeeded', self.name, "{}-{}".format( - listener.name, listener.unique_id), + channel.name, channel.unique_id), func_name) except Exception as ex: # pylint: disable=broad-except _LOGGER.warning( - '%s listener: %s %s stage failed ex: %s', + '%s channel: %s %s stage failed ex: %s', self.name, - "{}-{}".format(listener.name, listener.unique_id), + "{}-{}".format(channel.name, channel.unique_id), func_name, ex ) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 391b12189cf..4fbf96a22b6 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -18,15 +18,18 @@ from .const import ( ZHA_DISCOVERY_NEW, DEVICE_CLASS, SINGLE_INPUT_CLUSTER_DEVICE_CLASS, SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, COMPONENT_CLUSTERS, HUMIDITY, 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, 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_entity import ZhaDeviceEntity -from .listeners import ( - LISTENER_REGISTRY, AttributeListener, EventRelayListener, ZDOListener, - BasicListener) +from .channels import ( + AttributeListeningChannel, EventRelayChannel, ZDOChannel +) +from .channels.general import BasicChannel +from .channels.registry import ZIGBEE_CHANNEL_REGISTRY from .helpers import convert_ieee _LOGGER = logging.getLogger(__name__) @@ -34,7 +37,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = {} BINARY_SENSOR_TYPES = {} EntityReference = collections.namedtuple( - 'EntityReference', 'reference_id zha_device cluster_listeners device_info') + 'EntityReference', 'reference_id zha_device cluster_channels device_info') class ZHAGateway: @@ -106,14 +109,14 @@ class ZHAGateway: return self._device_registry def register_entity_reference( - self, ieee, reference_id, zha_device, cluster_listeners, + self, ieee, reference_id, zha_device, cluster_channels, 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, + cluster_channels=cluster_channels, device_info=device_info ) ) @@ -169,14 +172,14 @@ class ZHAGateway: # available and we already loaded fresh state above zha_device.update_available(True) 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 # powered device so we should be able to update it now _LOGGER.debug( "attempting to request fresh state for %s %s", zha_device.name, "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) @@ -188,11 +191,11 @@ class ZHAGateway: import zigpy.profiles if endpoint_id == 0: # ZDO - await _create_cluster_listener( + await _create_cluster_channel( endpoint, zha_device, is_new_join, - listener_class=ZDOListener + channel_class=ZDOChannel ) return @@ -234,18 +237,18 @@ class ZHAGateway: )) -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) +async def _create_cluster_channel(cluster, zha_device, is_new_join, + channels=None, channel_class=None): + """Create a cluster channel and attach it to a device.""" + if channel_class is None: + channel_class = ZIGBEE_CHANNEL_REGISTRY.get(cluster.cluster_id, + AttributeListeningChannel) + channel = channel_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) + await channel.async_configure() + zha_device.add_cluster_channel(channel) + if channels is not None: + channels.append(channel) 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] if c in endpoint.out_clusters] - listeners = [] + channels = [] cluster_tasks = [] for cluster in in_clusters: - cluster_tasks.append(_create_cluster_listener( - cluster, zha_device, is_new_join, listeners=listeners)) + cluster_tasks.append(_create_cluster_channel( + cluster, zha_device, is_new_join, channels=channels)) for cluster in out_clusters: - cluster_tasks.append(_create_cluster_listener( - cluster, zha_device, is_new_join, listeners=listeners)) + cluster_tasks.append(_create_cluster_channel( + cluster, zha_device, is_new_join, channels=channels)) await asyncio.gather(*cluster_tasks) discovery_info = { 'unique_id': device_key, 'zha_device': zha_device, - 'listeners': listeners, + 'channels': channels, 'component': component } @@ -314,7 +317,7 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device, """Dispatch single cluster matches to HA components.""" cluster_matches = [] cluster_match_tasks = [] - event_listener_tasks = [] + event_channel_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( @@ -327,7 +330,7 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device, )) 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, cluster, 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: - event_listener_tasks.append(_create_cluster_listener( + event_channel_tasks.append(_create_cluster_channel( cluster, zha_device, 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) for cluster_match in cluster_match_results: if cluster_match is not None: @@ -359,10 +362,10 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device, return cluster_matches -async def _handle_listener_only_cluster_match( +async def _handle_channel_only_cluster_match( zha_device, cluster, is_new_join): - """Handle a listener only cluster match.""" - await _create_cluster_listener(cluster, zha_device, is_new_join) + """Handle a channel only cluster match.""" + await _create_cluster_channel(cluster, zha_device, is_new_join) 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: return - listeners = [] - await _create_cluster_listener(cluster, zha_device, is_new_join, - listeners=listeners) + channels = [] + await _create_cluster_channel(cluster, zha_device, is_new_join, + channels=channels) cluster_key = "{}-{}".format(device_key, cluster.cluster_id) discovery_info = { 'unique_id': cluster_key, 'zha_device': zha_device, - 'listeners': listeners, + 'channels': channels, 'entity_suffix': '_{}'.format(cluster.cluster_id), 'component': component } @@ -403,11 +406,11 @@ async def _handle_single_cluster_match(hass, zha_device, cluster, device_key, 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) + device_entity_channels = [] + if POWER_CONFIGURATION_CHANNEL in zha_device.cluster_channels: + channel = zha_device.cluster_channels.get(POWER_CONFIGURATION_CHANNEL) + device_entity_channels.append(channel) + return ZhaDeviceEntity(zha_device, device_entity_channels) def establish_device_mappings(): diff --git a/homeassistant/components/zha/core/listeners.py b/homeassistant/components/zha/core/listeners.py deleted file mode 100644 index f8d24ce903c..00000000000 --- a/homeassistant/components/zha/core/listeners.py +++ /dev/null @@ -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 diff --git a/homeassistant/components/zha/device_entity.py b/homeassistant/components/zha/device_entity.py index e8b765a07a6..5632c849d59 100644 --- a/homeassistant/components/zha/device_entity.py +++ b/homeassistant/components/zha/device_entity.py @@ -11,7 +11,7 @@ import time from homeassistant.core import callback from homeassistant.util import slugify 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__) @@ -38,7 +38,7 @@ STATE_OFFLINE = 'offline' class ZhaDeviceEntity(ZhaEntity): """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): """Init ZHA endpoint entity.""" ieee = zha_device.ieee @@ -55,7 +55,7 @@ class ZhaDeviceEntity(ZhaEntity): unique_id = str(ieeetail) 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) self._keepalive_interval = keepalive_interval @@ -66,7 +66,8 @@ class ZhaDeviceEntity(ZhaEntity): 'rssi': zha_device.rssi, }) self._should_poll = True - self._battery_listener = self.cluster_listeners.get(LISTENER_BATTERY) + self._battery_channel = self.cluster_channels.get( + POWER_CONFIGURATION_CHANNEL) @property def state(self) -> str: @@ -97,9 +98,9 @@ class ZhaDeviceEntity(ZhaEntity): 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: + if self._battery_channel: await self.async_accept_signal( - self._battery_listener, SIGNAL_STATE_ATTR, + self._battery_channel, 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() @@ -114,7 +115,7 @@ class ZhaDeviceEntity(ZhaEntity): self._zha_device.update_available(False) else: self._zha_device.update_available(True) - if self._battery_listener: + if self._battery_channel: await self.async_get_latest_battery_reading() @callback @@ -127,14 +128,14 @@ class ZhaDeviceEntity(ZhaEntity): super().async_set_available(available) 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( + """Get initial battery level and battery info from channel cache.""" + battery_size = await self._battery_channel.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 = await self._battery_channel.get_attribute_value( 'battery_quantity') if battery_quantity is not None: self._device_state_attributes['battery_quantity'] = \ @@ -142,8 +143,8 @@ class ZhaDeviceEntity(ZhaEntity): 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( + """Get the latest battery reading from channels cache.""" + battery = await self._battery_channel.get_attribute_value( 'battery_percentage_remaining') if battery is not None: self._device_state_attributes['battery_level'] = battery diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index d914a76c4ce..2f5aed4ca29 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -27,7 +27,7 @@ class ZhaEntity(entity.Entity): _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): """Init ZHA entity.""" self._force_update = False @@ -48,25 +48,25 @@ class ZhaEntity(entity.Entity): slugify(zha_device.manufacturer), slugify(zha_device.model), ieeetail, - listeners[0].cluster.endpoint.endpoint_id, + channels[0].cluster.endpoint.endpoint_id, kwargs.get(ENTITY_SUFFIX, ''), ) else: self.entity_id = "{}.zha_{}_{}{}".format( self._domain, ieeetail, - listeners[0].cluster.endpoint.endpoint_id, + channels[0].cluster.endpoint.endpoint_id, kwargs.get(ENTITY_SUFFIX, ''), ) self._state = None self._device_state_attributes = {} self._zha_device = zha_device - self.cluster_listeners = {} + self.cluster_channels = {} self._available = False self._component = kwargs['component'] self._unsubs = [] - for listener in listeners: - self.cluster_listeners[listener.name] = listener + for channel in channels: + self.cluster_channels[channel.name] = channel @property def name(self): @@ -147,7 +147,7 @@ class ZhaEntity(entity.Entity): ) self._zha_device.gateway.register_entity_reference( 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: """Disconnect entity object when removed.""" @@ -156,13 +156,13 @@ class ZhaEntity(entity.Entity): async def async_update(self): """Retrieve latest state.""" - for listener in self.cluster_listeners: - if hasattr(listener, 'async_update'): - await listener.async_update() + for channel in self.cluster_channels: + if hasattr(channel, '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): - """Accept a signal from a listener.""" + """Accept a signal from a channel.""" unsub = None if signal_override: unsub = async_dispatcher_connect( @@ -173,7 +173,7 @@ class ZhaEntity(entity.Entity): else: unsub = async_dispatcher_connect( self.hass, - "{}_{}".format(listener.unique_id, signal), + "{}_{}".format(channel.unique_id, signal), func ) self._unsubs.append(unsub) diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index dfe3c8cdd23..761dfaede1e 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -11,7 +11,7 @@ from homeassistant.components.fan import ( FanEntity) from homeassistant.helpers.dispatcher import async_dispatcher_connect 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 ) from .entity import ZhaEntity @@ -81,16 +81,16 @@ class ZhaFan(ZhaEntity, FanEntity): _domain = DOMAIN - def __init__(self, unique_id, zha_device, listeners, **kwargs): + def __init__(self, unique_id, zha_device, channels, **kwargs): """Init this sensor.""" - super().__init__(unique_id, zha_device, listeners, **kwargs) - self._fan_listener = self.cluster_listeners.get(LISTENER_FAN) + super().__init__(unique_id, zha_device, channels, **kwargs) + self._fan_channel = self.cluster_channels.get(FAN_CHANNEL) 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) + self._fan_channel, SIGNAL_ATTR_UPDATED, self.async_set_state) @property def supported_features(self) -> int: @@ -120,7 +120,7 @@ class ZhaFan(ZhaEntity, FanEntity): return self.state_attributes 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.async_schedule_update_ha_state() @@ -137,5 +137,5 @@ class ZhaFan(ZhaEntity, FanEntity): async def async_set_speed(self, speed: str) -> None: """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) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 09f1812cd76..efa6f679ae8 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -10,8 +10,8 @@ from homeassistant.components import light from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util from .const import ( - DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, LISTENER_COLOR, - LISTENER_ON_OFF, LISTENER_LEVEL, SIGNAL_ATTR_UPDATED, SIGNAL_SET_LEVEL + DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, COLOR_CHANNEL, + ON_OFF_CHANNEL, LEVEL_CHANNEL, SIGNAL_ATTR_UPDATED, SIGNAL_SET_LEVEL ) from .entity import ZhaEntity @@ -67,24 +67,24 @@ class Light(ZhaEntity, light.Light): _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.""" - super().__init__(unique_id, zha_device, listeners, **kwargs) + super().__init__(unique_id, zha_device, channels, **kwargs) self._supported_features = 0 self._color_temp = None self._hs_color = None self._brightness = None - 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) + self._on_off_channel = self.cluster_channels.get(ON_OFF_CHANNEL) + self._level_channel = self.cluster_channels.get(LEVEL_CHANNEL) + 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_TRANSITION self._brightness = 0 - if self._color_listener: - color_capabilities = self._color_listener.get_color_capabilities() + if self._color_channel: + color_capabilities = self._color_channel.get_color_capabilities() if color_capabilities & CAPABILITIES_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.""" 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: + self._on_off_channel, SIGNAL_ATTR_UPDATED, self.async_set_state) + if self._level_channel: 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): """Turn the entity on.""" @@ -152,7 +152,7 @@ class Light(ZhaEntity, light.Light): 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( + success = await self._color_channel.move_to_color_temp( temperature, duration) if not success: return @@ -162,7 +162,7 @@ class Light(ZhaEntity, light.Light): 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( + success = await self._color_channel.move_to_color( int(xy_color[0] * 65535), int(xy_color[1] * 65535), duration, @@ -174,7 +174,7 @@ class Light(ZhaEntity, light.Light): 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( + success = await self._level_channel.move_to_level_with_on_off( brightness, duration ) @@ -185,7 +185,7 @@ class Light(ZhaEntity, light.Light): self.async_schedule_update_ha_state() return - success = await self._on_off_listener.on() + success = await self._on_off_channel.on() if not success: return @@ -198,12 +198,12 @@ class Light(ZhaEntity, light.Light): 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( + success = await self._level_channel.move_to_level_with_on_off( 0, duration*10 ) 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) if not success: return diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 9c00d8124bb..6dcdbb845dc 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -12,7 +12,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core.const import ( DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, HUMIDITY, TEMPERATURE, 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) from .entity import ZhaEntity @@ -74,8 +74,8 @@ UNIT_REGISTRY = { GENERIC: None } -LISTENER_REGISTRY = { - ELECTRICAL_MEASUREMENT: LISTENER_ACTIVE_POWER, +CHANNEL_REGISTRY = { + ELECTRICAL_MEASUREMENT: ELECTRICAL_MEASUREMENT_CHANNEL, } POLLING_REGISTRY = { @@ -130,9 +130,9 @@ class Sensor(ZhaEntity): _domain = DOMAIN - def __init__(self, unique_id, zha_device, listeners, **kwargs): + def __init__(self, unique_id, zha_device, channels, **kwargs): """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) self._unit = UNIT_REGISTRY.get(sensor_type) self._formatter_function = FORMATTER_FUNC_REGISTRY.get( @@ -147,17 +147,17 @@ class Sensor(ZhaEntity): sensor_type, False ) - self._listener = self.cluster_listeners.get( - LISTENER_REGISTRY.get(sensor_type, LISTENER_ATTRIBUTE) + self._channel = self.cluster_channels.get( + CHANNEL_REGISTRY.get(sensor_type, ATTRIBUTE_CHANNEL) ) 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) + self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state) await self.async_accept_signal( - self._listener, SIGNAL_STATE_ATTR, + self._channel, SIGNAL_STATE_ATTR, self.async_update_state_attribute) @property @@ -175,6 +175,6 @@ class Sensor(ZhaEntity): return 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.async_schedule_update_ha_state() diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 4eee3d5da35..bdbdd7a6a76 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -9,7 +9,7 @@ import logging from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.helpers.dispatcher import async_dispatcher_connect 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 ) from .entity import ZhaEntity @@ -60,7 +60,7 @@ class Switch(ZhaEntity, SwitchDevice): def __init__(self, **kwargs): """Initialize the ZHA switch.""" 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 def is_on(self) -> bool: @@ -71,14 +71,14 @@ class Switch(ZhaEntity, SwitchDevice): async def async_turn_on(self, **kwargs): """Turn the entity on.""" - await self._on_off_listener.on() + await self._on_off_channel.on() async def async_turn_off(self, **kwargs): """Turn the entity off.""" - await self._on_off_listener.off() + await self._on_off_channel.off() def async_set_state(self, state): - """Handle state update from listener.""" + """Handle state update from channel.""" self._state = bool(state) self.async_schedule_update_ha_state() @@ -91,4 +91,4 @@ class Switch(ZhaEntity, SwitchDevice): """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) + self._on_off_channel, SIGNAL_ATTR_UPDATED, self.async_set_state) diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index c806b1a2217..bd594941da1 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -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 establish_device_mappings -from homeassistant.components.zha.core.listeners \ - import populate_listener_registry +from homeassistant.components.zha.core.channels.registry \ + import populate_channel_registry 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 had a real zigbee network running. """ - populate_listener_registry() + populate_channel_registry() establish_device_mappings() for component in COMPONENTS: hass.data[DATA_ZHA][component] = (