ZHA - Event foundation (#19095)

* event foundation

* add missing periods to comments

* reworked so that entities don't fire events

* lint

* review comments
pull/19099/head
David F. Mulcahey 2018-12-10 07:59:50 -08:00 committed by Charles Garwood
parent 59581786d3
commit f4f42176bd
5 changed files with 121 additions and 1 deletions

View File

@ -190,6 +190,10 @@ class Remote(ZhaEntity, BinarySensorDevice):
"""Handle ZDO commands on this cluster."""
pass
def zha_send_event(self, cluster, command, args):
"""Relay entity events to hass."""
pass # don't let entities fire events
class LevelListener:
"""Listener for the LevelControl Zigbee cluster."""
@ -220,6 +224,10 @@ class Remote(ZhaEntity, BinarySensorDevice):
"""Handle ZDO commands on this cluster."""
pass
def zha_send_event(self, cluster, command, args):
"""Relay entity events to hass."""
pass # don't let entities fire events
def __init__(self, **kwargs):
"""Initialize Switch."""
super().__init__(**kwargs)

View File

@ -7,6 +7,7 @@ https://home-assistant.io/components/zha/
import collections
import logging
import os
import types
import voluptuous as vol
@ -20,12 +21,14 @@ from homeassistant.helpers.entity_component import EntityComponent
# Loading the config flow file will register the flow
from . import config_flow # noqa # pylint: disable=unused-import
from . import const as zha_const
from .event import ZhaEvent
from .const import (
COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG,
CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, DATA_ZHA_BRIDGE_ID,
DATA_ZHA_CONFIG, DATA_ZHA_CORE_COMPONENT, DATA_ZHA_DISPATCHERS,
DATA_ZHA_RADIO, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME,
DEFAULT_RADIO_TYPE, DOMAIN, ZHA_DISCOVERY_NEW, RadioType)
DEFAULT_RADIO_TYPE, DOMAIN, ZHA_DISCOVERY_NEW, RadioType,
EVENTABLE_CLUSTERS, DATA_ZHA_CORE_EVENTS)
REQUIREMENTS = [
'bellows==0.7.0',
@ -130,6 +133,19 @@ async def async_setup_entry(hass, config_entry):
database = config[CONF_DATABASE]
else:
database = os.path.join(hass.config.config_dir, DEFAULT_DATABASE_NAME)
# patch zigpy listener to prevent flooding logs with warnings due to
# how zigpy implemented its listeners
from zigpy.appdb import ClusterPersistingListener
def zha_send_event(self, cluster, command, args):
pass
ClusterPersistingListener.zha_send_event = types.MethodType(
zha_send_event,
ClusterPersistingListener
)
APPLICATION_CONTROLLER = ControllerApplication(radio, database)
listener = ApplicationListener(hass, config)
APPLICATION_CONTROLLER.add_listener(listener)
@ -205,6 +221,9 @@ async def async_unload_entry(hass, config_entry):
for entity_id in entity_ids:
await component.async_remove_entity(entity_id)
# clean up events
hass.data[DATA_ZHA][DATA_ZHA_CORE_EVENTS].clear()
_LOGGER.debug("Closing zha radio")
hass.data[DATA_ZHA][DATA_ZHA_RADIO].close()
@ -221,6 +240,7 @@ class ApplicationListener:
self._config = config
self._component = EntityComponent(_LOGGER, DOMAIN, hass)
self._device_registry = collections.defaultdict(list)
self._events = {}
zha_const.populate_data()
for component in COMPONENTS:
@ -228,6 +248,7 @@ class ApplicationListener:
hass.data[DATA_ZHA].get(component, {})
)
hass.data[DATA_ZHA][DATA_ZHA_CORE_COMPONENT] = self._component
hass.data[DATA_ZHA][DATA_ZHA_CORE_EVENTS] = self._events
def device_joined(self, device):
"""Handle device joined.
@ -256,6 +277,8 @@ class ApplicationListener:
"""Handle device being removed from the network."""
for device_entity in self._device_registry[device.ieee]:
self._hass.async_create_task(device_entity.async_remove())
if device.ieee in self._events:
self._events.pop(device.ieee)
async def async_device_initialized(self, device, join):
"""Handle device joined and basic information discovered (async)."""
@ -362,6 +385,14 @@ class ApplicationListener:
device_classes, discovery_attr,
is_new_join):
"""Try to set up an entity from a "bare" cluster."""
if cluster.cluster_id in EVENTABLE_CLUSTERS:
if cluster.endpoint.device.ieee not in self._events:
self._events.update({cluster.endpoint.device.ieee: []})
self._events[cluster.endpoint.device.ieee].append(ZhaEvent(
self._hass,
cluster
))
if cluster.cluster_id in profile_clusters:
return

View File

@ -13,6 +13,7 @@ DATA_ZHA_BRIDGE_ID = 'zha_bridge_id'
DATA_ZHA_RADIO = 'zha_radio'
DATA_ZHA_DISPATCHERS = 'zha_dispatchers'
DATA_ZHA_CORE_COMPONENT = 'zha_core_component'
DATA_ZHA_CORE_EVENTS = 'zha_core_events'
ZHA_DISCOVERY_NEW = 'zha_discovery_new_{}'
COMPONENTS = [
@ -53,6 +54,7 @@ SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {}
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {}
CUSTOM_CLUSTER_MAPPINGS = {}
COMPONENT_CLUSTERS = {}
EVENTABLE_CLUSTERS = []
def populate_data():
@ -70,6 +72,11 @@ def populate_data():
if zll.PROFILE_ID not in DEVICE_CLASS:
DEVICE_CLASS[zll.PROFILE_ID] = {}
EVENTABLE_CLUSTERS.append(zcl.clusters.general.AnalogInput.cluster_id)
EVENTABLE_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id)
EVENTABLE_CLUSTERS.append(zcl.clusters.general.MultistateInput.cluster_id)
EVENTABLE_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id)
DEVICE_CLASS[zha.PROFILE_ID].update({
zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor',
zha.DeviceType.LEVEL_CONTROL_SWITCH: 'binary_sensor',

View File

@ -103,3 +103,8 @@ class ZhaEntity(entity.Entity):
'name': self._device_state_attributes['friendly_name'],
'via_hub': (DOMAIN, self.hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID]),
}
@callback
def zha_send_event(self, cluster, command, args):
"""Relay entity events to hass."""
pass # don't relay events from entities

View File

@ -0,0 +1,69 @@
"""
Event 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 EventOrigin, callback
from homeassistant.util import slugify
_LOGGER = logging.getLogger(__name__)
class ZhaEvent():
"""A base class for ZHA events."""
def __init__(self, hass, cluster, **kwargs):
"""Init ZHA event."""
self._hass = hass
self._cluster = cluster
cluster.add_listener(self)
ieee = cluster.endpoint.device.ieee
ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
endpoint = cluster.endpoint
if endpoint.manufacturer and endpoint.model is not None:
self._unique_id = "{}.{}_{}_{}_{}{}".format(
'zha_event',
slugify(endpoint.manufacturer),
slugify(endpoint.model),
ieeetail,
cluster.endpoint.endpoint_id,
kwargs.get('entity_suffix', ''),
)
else:
self._unique_id = "{}.zha_{}_{}{}".format(
'zha_event',
ieeetail,
cluster.endpoint.endpoint_id,
kwargs.get('entity_suffix', ''),
)
@callback
def attribute_updated(self, attribute, value):
"""Handle an attribute updated on this cluster."""
pass
@callback
def zdo_command(self, tsn, command_id, args):
"""Handle a ZDO command received on this cluster."""
pass
@callback
def cluster_command(self, tsn, command_id, args):
"""Handle a cluster command received on this cluster."""
pass
@callback
def zha_send_event(self, cluster, command, args):
"""Relay entity events to hass."""
self._hass.bus.async_fire(
'zha_event',
{
'unique_id': self._unique_id,
'command': command,
'args': args
},
EventOrigin.remote
)