mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
zha: Support remotes/buttons (#12528)
This commit is contained in:
parent
30d987f59f
commit
02a12a0bb4
@ -31,12 +31,21 @@ async def async_setup_platform(hass, config, async_add_devices,
|
|||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
from zigpy.zcl.clusters.general import OnOff
|
||||||
from zigpy.zcl.clusters.security import IasZone
|
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
|
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']:
|
if discovery_info['new_join']:
|
||||||
await cluster.bind()
|
await cluster.bind()
|
||||||
ieee = cluster.endpoint.device.application.ieee
|
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_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):
|
class BinarySensor(zha.Entity, BinarySensorDevice):
|
||||||
"""THe ZHA Binary Sensor."""
|
"""The ZHA Binary Sensor."""
|
||||||
|
|
||||||
_domain = DOMAIN
|
_domain = DOMAIN
|
||||||
|
|
||||||
@ -102,3 +137,114 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
|
|||||||
state = result.get('zone_status', self._state)
|
state = result.get('zone_status', self._state)
|
||||||
if isinstance(state, (int, uint16_t)):
|
if isinstance(state, (int, uint16_t)):
|
||||||
self._state = result.get('zone_status', self._state) & 3
|
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)
|
||||||
|
@ -221,44 +221,78 @@ class ApplicationListener:
|
|||||||
self._config,
|
self._config,
|
||||||
)
|
)
|
||||||
|
|
||||||
for cluster_id, cluster in endpoint.in_clusters.items():
|
for cluster in endpoint.in_clusters.values():
|
||||||
cluster_type = type(cluster)
|
await self._attempt_single_cluster_device(
|
||||||
if cluster_id in profile_clusters[0]:
|
endpoint,
|
||||||
continue
|
cluster,
|
||||||
if cluster_type not in zha_const.SINGLE_CLUSTER_DEVICE_CLASS:
|
profile_clusters[0],
|
||||||
continue
|
device_key,
|
||||||
|
zha_const.SINGLE_INPUT_CLUSTER_DEVICE_CLASS,
|
||||||
|
'in_clusters',
|
||||||
|
discovered_info,
|
||||||
|
join,
|
||||||
|
)
|
||||||
|
|
||||||
component = zha_const.SINGLE_CLUSTER_DEVICE_CLASS[cluster_type]
|
for cluster in endpoint.out_clusters.values():
|
||||||
cluster_key = "{}-{}".format(device_key, cluster_id)
|
await self._attempt_single_cluster_device(
|
||||||
discovery_info = {
|
endpoint,
|
||||||
'application_listener': self,
|
cluster,
|
||||||
'endpoint': endpoint,
|
profile_clusters[1],
|
||||||
'in_clusters': {cluster.cluster_id: cluster},
|
device_key,
|
||||||
'out_clusters': {},
|
zha_const.SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS,
|
||||||
'new_join': join,
|
'out_clusters',
|
||||||
'unique_id': cluster_key,
|
discovered_info,
|
||||||
'entity_suffix': '_{}'.format(cluster_id),
|
join,
|
||||||
}
|
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def register_entity(self, ieee, entity_obj):
|
def register_entity(self, ieee, entity_obj):
|
||||||
"""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(entity_obj)
|
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):
|
class Entity(entity.Entity):
|
||||||
"""A base class for ZHA entities."""
|
"""A base class for ZHA entities."""
|
||||||
|
|
||||||
_domain = None # Must be overridden by subclasses
|
_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,
|
def __init__(self, endpoint, in_clusters, out_clusters, manufacturer,
|
||||||
model, application_listener, unique_id, **kwargs):
|
model, application_listener, unique_id, **kwargs):
|
||||||
@ -287,10 +321,11 @@ class Entity(entity.Entity):
|
|||||||
kwargs.get('entity_suffix', ''),
|
kwargs.get('entity_suffix', ''),
|
||||||
)
|
)
|
||||||
|
|
||||||
for cluster in in_clusters.values():
|
for cluster_id, cluster in in_clusters.items():
|
||||||
cluster.add_listener(self)
|
cluster.add_listener(self._in_listeners.get(cluster_id, self))
|
||||||
for cluster in out_clusters.values():
|
for cluster_id, cluster in out_clusters.items():
|
||||||
cluster.add_listener(self)
|
cluster.add_listener(self._out_listeners.get(cluster_id, self))
|
||||||
|
|
||||||
self._endpoint = endpoint
|
self._endpoint = endpoint
|
||||||
self._in_clusters = in_clusters
|
self._in_clusters = in_clusters
|
||||||
self._out_clusters = out_clusters
|
self._out_clusters = out_clusters
|
||||||
@ -379,7 +414,7 @@ async def safe_read(cluster, attributes):
|
|||||||
try:
|
try:
|
||||||
result, _ = await cluster.read_attributes(
|
result, _ = await cluster.read_attributes(
|
||||||
attributes,
|
attributes,
|
||||||
allow_cache=False,
|
allow_cache=True,
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
"""All constants related to the ZHA component."""
|
"""All constants related to the ZHA component."""
|
||||||
|
|
||||||
DEVICE_CLASS = {}
|
DEVICE_CLASS = {}
|
||||||
SINGLE_CLUSTER_DEVICE_CLASS = {}
|
SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {}
|
||||||
|
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {}
|
||||||
COMPONENT_CLUSTERS = {}
|
COMPONENT_CLUSTERS = {}
|
||||||
|
|
||||||
|
|
||||||
@ -15,11 +16,17 @@ def populate_data():
|
|||||||
from zigpy.profiles import PROFILES, zha, zll
|
from zigpy.profiles import PROFILES, zha, zll
|
||||||
|
|
||||||
DEVICE_CLASS[zha.PROFILE_ID] = {
|
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.SMART_PLUG: 'switch',
|
||||||
|
|
||||||
zha.DeviceType.ON_OFF_LIGHT: 'light',
|
zha.DeviceType.ON_OFF_LIGHT: 'light',
|
||||||
zha.DeviceType.DIMMABLE_LIGHT: 'light',
|
zha.DeviceType.DIMMABLE_LIGHT: 'light',
|
||||||
zha.DeviceType.COLOR_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] = {
|
DEVICE_CLASS[zll.PROFILE_ID] = {
|
||||||
zll.DeviceType.ON_OFF_LIGHT: 'light',
|
zll.DeviceType.ON_OFF_LIGHT: 'light',
|
||||||
@ -29,15 +36,23 @@ def populate_data():
|
|||||||
zll.DeviceType.COLOR_LIGHT: 'light',
|
zll.DeviceType.COLOR_LIGHT: 'light',
|
||||||
zll.DeviceType.EXTENDED_COLOR_LIGHT: 'light',
|
zll.DeviceType.EXTENDED_COLOR_LIGHT: 'light',
|
||||||
zll.DeviceType.COLOR_TEMPERATURE_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.general.OnOff: 'switch',
|
||||||
zcl.clusters.measurement.RelativeHumidity: 'sensor',
|
zcl.clusters.measurement.RelativeHumidity: 'sensor',
|
||||||
zcl.clusters.measurement.TemperatureMeasurement: 'sensor',
|
zcl.clusters.measurement.TemperatureMeasurement: 'sensor',
|
||||||
zcl.clusters.security.IasZone: 'binary_sensor',
|
zcl.clusters.security.IasZone: 'binary_sensor',
|
||||||
zcl.clusters.hvac.Fan: 'fan',
|
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
|
# A map of hass components to all Zigbee clusters it could use
|
||||||
for profile_id, classes in DEVICE_CLASS.items():
|
for profile_id, classes in DEVICE_CLASS.items():
|
||||||
|
Loading…
x
Reference in New Issue
Block a user