zha: Support remotes/buttons (#12528)

This commit is contained in:
Russell Cloran 2018-04-29 23:31:27 -07:00 committed by GitHub
parent 30d987f59f
commit 02a12a0bb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 232 additions and 36 deletions

View File

@ -31,12 +31,21 @@ async def async_setup_platform(hass, config, async_add_devices,
if discovery_info is None:
return
from zigpy.zcl.clusters.general import OnOff
from zigpy.zcl.clusters.security import IasZone
if IasZone.cluster_id in discovery_info['in_clusters']:
await _async_setup_iaszone(hass, config, async_add_devices,
discovery_info)
elif OnOff.cluster_id in discovery_info['out_clusters']:
await _async_setup_remote(hass, config, async_add_devices,
discovery_info)
in_clusters = discovery_info['in_clusters']
async def _async_setup_iaszone(hass, config, async_add_devices,
discovery_info):
device_class = None
cluster = in_clusters[IasZone.cluster_id]
from zigpy.zcl.clusters.security import IasZone
cluster = discovery_info['in_clusters'][IasZone.cluster_id]
if discovery_info['new_join']:
await cluster.bind()
ieee = cluster.endpoint.device.application.ieee
@ -53,8 +62,34 @@ async def async_setup_platform(hass, config, async_add_devices,
async_add_devices([sensor], update_before_add=True)
async def _async_setup_remote(hass, config, async_add_devices, discovery_info):
async def safe(coro):
"""Run coro, catching ZigBee delivery errors, and ignoring them."""
import zigpy.exceptions
try:
await coro
except zigpy.exceptions.DeliveryError as exc:
_LOGGER.warning("Ignoring error during setup: %s", exc)
if discovery_info['new_join']:
from zigpy.zcl.clusters.general import OnOff, LevelControl
out_clusters = discovery_info['out_clusters']
if OnOff.cluster_id in out_clusters:
cluster = out_clusters[OnOff.cluster_id]
await safe(cluster.bind())
await safe(cluster.configure_reporting(0, 0, 600, 1))
if LevelControl.cluster_id in out_clusters:
cluster = out_clusters[LevelControl.cluster_id]
await safe(cluster.bind())
await safe(cluster.configure_reporting(0, 1, 600, 1))
sensor = Switch(**discovery_info)
async_add_devices([sensor], update_before_add=True)
class BinarySensor(zha.Entity, BinarySensorDevice):
"""THe ZHA Binary Sensor."""
"""The ZHA Binary Sensor."""
_domain = DOMAIN
@ -102,3 +137,114 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
state = result.get('zone_status', self._state)
if isinstance(state, (int, uint16_t)):
self._state = result.get('zone_status', self._state) & 3
class Switch(zha.Entity, BinarySensorDevice):
"""ZHA switch/remote controller/button."""
_domain = DOMAIN
class OnOffListener:
"""Listener for the OnOff ZigBee cluster."""
def __init__(self, entity):
"""Initialize OnOffListener."""
self._entity = entity
def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster."""
if command_id in (0x0000, 0x0040):
self._entity.set_state(False)
elif command_id in (0x0001, 0x0041, 0x0042):
self._entity.set_state(True)
elif command_id == 0x0002:
self._entity.set_state(not self._entity.is_on)
def attribute_updated(self, attrid, value):
"""Handle attribute updates on this cluster."""
if attrid == 0:
self._entity.set_state(value)
self._entity.schedule_update_ha_state()
def zdo_command(self, *args, **kwargs):
"""Handle ZDO commands on this cluster."""
pass
class LevelListener:
"""Listener for the LevelControl ZigBee cluster."""
def __init__(self, entity):
"""Initialize LevelListener."""
self._entity = entity
def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster."""
if command_id in (0x0000, 0x0004): # move_to_level, -with_on_off
self._entity.set_level(args[0])
elif command_id in (0x0001, 0x0005): # move, -with_on_off
# We should dim slowly -- for now, just step once
rate = args[1]
if args[0] == 0xff:
rate = 10 # Should read default move rate
self._entity.move_level(-rate if args[0] else rate)
elif command_id == 0x0002: # step
# Step (technically shouldn't change on/off)
self._entity.move_level(-args[1] if args[0] else args[1])
def attribute_update(self, attrid, value):
"""Handle attribute updates on this cluster."""
if attrid == 0:
self._entity.set_level(value)
def zdo_command(self, *args, **kwargs):
"""Handle ZDO commands on this cluster."""
pass
def __init__(self, **kwargs):
"""Initialize Switch."""
self._state = True
self._level = 255
from zigpy.zcl.clusters import general
self._out_listeners = {
general.OnOff.cluster_id: self.OnOffListener(self),
general.LevelControl.cluster_id: self.LevelListener(self),
}
super().__init__(**kwargs)
@property
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
return self._state
@property
def device_state_attributes(self):
"""Return the device state attributes."""
return {'level': self._state and self._level or 0}
def move_level(self, change):
"""Increment the level, setting state if appropriate."""
if not self._state and change > 0:
self._level = 0
self._level = min(255, max(0, self._level + change))
self._state = bool(self._level)
self.schedule_update_ha_state()
def set_level(self, level):
"""Set the level, setting state if appropriate."""
self._level = level
self._state = bool(self._level)
self.schedule_update_ha_state()
def set_state(self, state):
"""Set the state."""
self._state = state
if self._level == 0:
self._level = 255
self.schedule_update_ha_state()
async def async_update(self):
"""Retrieve latest state."""
from zigpy.zcl.clusters.general import OnOff
result = await zha.safe_read(
self._endpoint.out_clusters[OnOff.cluster_id], ['on_off'])
self._state = result.get('on_off', self._state)

View File

@ -221,44 +221,78 @@ class ApplicationListener:
self._config,
)
for cluster_id, cluster in endpoint.in_clusters.items():
cluster_type = type(cluster)
if cluster_id in profile_clusters[0]:
continue
if cluster_type not in zha_const.SINGLE_CLUSTER_DEVICE_CLASS:
continue
for cluster in endpoint.in_clusters.values():
await self._attempt_single_cluster_device(
endpoint,
cluster,
profile_clusters[0],
device_key,
zha_const.SINGLE_INPUT_CLUSTER_DEVICE_CLASS,
'in_clusters',
discovered_info,
join,
)
component = zha_const.SINGLE_CLUSTER_DEVICE_CLASS[cluster_type]
cluster_key = "{}-{}".format(device_key, cluster_id)
discovery_info = {
'application_listener': self,
'endpoint': endpoint,
'in_clusters': {cluster.cluster_id: cluster},
'out_clusters': {},
'new_join': join,
'unique_id': cluster_key,
'entity_suffix': '_{}'.format(cluster_id),
}
discovery_info.update(discovered_info)
self._hass.data[DISCOVERY_KEY][cluster_key] = discovery_info
await discovery.async_load_platform(
self._hass,
component,
DOMAIN,
{'discovery_key': cluster_key},
self._config,
for cluster in endpoint.out_clusters.values():
await self._attempt_single_cluster_device(
endpoint,
cluster,
profile_clusters[1],
device_key,
zha_const.SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS,
'out_clusters',
discovered_info,
join,
)
def register_entity(self, ieee, entity_obj):
"""Record the creation of a hass entity associated with ieee."""
self._device_registry[ieee].append(entity_obj)
async def _attempt_single_cluster_device(self, endpoint, cluster,
profile_clusters, device_key,
device_classes, discovery_attr,
entity_info, is_new_join):
"""Try to set up an entity from a "bare" cluster."""
if cluster.cluster_id in profile_clusters:
return
# pylint: disable=unidiomatic-typecheck
if type(cluster) not in device_classes:
return
component = device_classes[type(cluster)]
cluster_key = "{}-{}".format(device_key, cluster.cluster_id)
discovery_info = {
'application_listener': self,
'endpoint': endpoint,
'in_clusters': {},
'out_clusters': {},
'new_join': is_new_join,
'unique_id': cluster_key,
'entity_suffix': '_{}'.format(cluster.cluster_id),
}
discovery_info[discovery_attr] = {cluster.cluster_id: cluster}
discovery_info.update(entity_info)
self._hass.data[DISCOVERY_KEY][cluster_key] = discovery_info
await discovery.async_load_platform(
self._hass,
component,
DOMAIN,
{'discovery_key': cluster_key},
self._config,
)
class Entity(entity.Entity):
"""A base class for ZHA entities."""
_domain = None # Must be overridden by subclasses
# Normally the entity itself is the listener. Base classes may set this to
# a dict of cluster ID -> listener to receive messages for specific
# clusters separately
_in_listeners = {}
_out_listeners = {}
def __init__(self, endpoint, in_clusters, out_clusters, manufacturer,
model, application_listener, unique_id, **kwargs):
@ -287,10 +321,11 @@ class Entity(entity.Entity):
kwargs.get('entity_suffix', ''),
)
for cluster in in_clusters.values():
cluster.add_listener(self)
for cluster in out_clusters.values():
cluster.add_listener(self)
for cluster_id, cluster in in_clusters.items():
cluster.add_listener(self._in_listeners.get(cluster_id, self))
for cluster_id, cluster in out_clusters.items():
cluster.add_listener(self._out_listeners.get(cluster_id, self))
self._endpoint = endpoint
self._in_clusters = in_clusters
self._out_clusters = out_clusters
@ -379,7 +414,7 @@ async def safe_read(cluster, attributes):
try:
result, _ = await cluster.read_attributes(
attributes,
allow_cache=False,
allow_cache=True,
)
return result
except Exception: # pylint: disable=broad-except

View File

@ -1,7 +1,8 @@
"""All constants related to the ZHA component."""
DEVICE_CLASS = {}
SINGLE_CLUSTER_DEVICE_CLASS = {}
SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {}
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {}
COMPONENT_CLUSTERS = {}
@ -15,11 +16,17 @@ def populate_data():
from zigpy.profiles import PROFILES, zha, zll
DEVICE_CLASS[zha.PROFILE_ID] = {
zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor',
zha.DeviceType.LEVEL_CONTROL_SWITCH: 'binary_sensor',
zha.DeviceType.REMOTE_CONTROL: 'binary_sensor',
zha.DeviceType.SMART_PLUG: 'switch',
zha.DeviceType.ON_OFF_LIGHT: 'light',
zha.DeviceType.DIMMABLE_LIGHT: 'light',
zha.DeviceType.COLOR_DIMMABLE_LIGHT: 'light',
zha.DeviceType.ON_OFF_LIGHT_SWITCH: 'binary_sensor',
zha.DeviceType.DIMMER_SWITCH: 'binary_sensor',
zha.DeviceType.COLOR_DIMMER_SWITCH: 'binary_sensor',
}
DEVICE_CLASS[zll.PROFILE_ID] = {
zll.DeviceType.ON_OFF_LIGHT: 'light',
@ -29,15 +36,23 @@ def populate_data():
zll.DeviceType.COLOR_LIGHT: 'light',
zll.DeviceType.EXTENDED_COLOR_LIGHT: 'light',
zll.DeviceType.COLOR_TEMPERATURE_LIGHT: 'light',
zll.DeviceType.COLOR_CONTROLLER: 'binary_sensor',
zll.DeviceType.COLOR_SCENE_CONTROLLER: 'binary_sensor',
zll.DeviceType.CONTROLLER: 'binary_sensor',
zll.DeviceType.SCENE_CONTROLLER: 'binary_sensor',
zll.DeviceType.ON_OFF_SENSOR: 'binary_sensor',
}
SINGLE_CLUSTER_DEVICE_CLASS.update({
SINGLE_INPUT_CLUSTER_DEVICE_CLASS.update({
zcl.clusters.general.OnOff: 'switch',
zcl.clusters.measurement.RelativeHumidity: 'sensor',
zcl.clusters.measurement.TemperatureMeasurement: 'sensor',
zcl.clusters.security.IasZone: 'binary_sensor',
zcl.clusters.hvac.Fan: 'fan',
})
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({
zcl.clusters.general.OnOff: 'binary_sensor',
})
# A map of hass components to all Zigbee clusters it could use
for profile_id, classes in DEVICE_CLASS.items():