Refactor ZHA listeners into channels (#21196)
* refactor listeners to channels * update coveragerc
This commit is contained in:
parent
fe4a2b5b31
commit
3be8178035
28 changed files with 1037 additions and 899 deletions
|
@ -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
|
||||||
|
|
|
@ -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] = (
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
308
homeassistant/components/zha/core/channels/__init__.py
Normal file
308
homeassistant/components/zha/core/channels/__init__.py
Normal 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
|
||||||
|
)
|
9
homeassistant/components/zha/core/channels/closures.py
Normal file
9
homeassistant/components/zha/core/channels/closures.py
Normal 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__)
|
202
homeassistant/components/zha/core/channels/general.py
Normal file
202
homeassistant/components/zha/core/channels/general.py
Normal 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)
|
40
homeassistant/components/zha/core/channels/homeautomation.py
Normal file
40
homeassistant/components/zha/core/channels/homeautomation.py
Normal 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)
|
62
homeassistant/components/zha/core/channels/hvac.py
Normal file
62
homeassistant/components/zha/core/channels/hvac.py
Normal 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)
|
48
homeassistant/components/zha/core/channels/lighting.py
Normal file
48
homeassistant/components/zha/core/channels/lighting.py
Normal 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)
|
9
homeassistant/components/zha/core/channels/lightlink.py
Normal file
9
homeassistant/components/zha/core/channels/lightlink.py
Normal 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__)
|
|
@ -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__)
|
|
@ -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__)
|
9
homeassistant/components/zha/core/channels/protocol.py
Normal file
9
homeassistant/components/zha/core/channels/protocol.py
Normal 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__)
|
46
homeassistant/components/zha/core/channels/registry.py
Normal file
46
homeassistant/components/zha/core/channels/registry.py
Normal 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,
|
||||||
|
})
|
82
homeassistant/components/zha/core/channels/security.py
Normal file
82
homeassistant/components/zha/core/channels/security.py
Normal 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)
|
|
@ -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__)
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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] = (
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue