From f4f42176bdfce4fbf3d16c0646f607b83c01e0f0 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 10 Dec 2018 07:59:50 -0800 Subject: [PATCH] ZHA - Event foundation (#19095) * event foundation * add missing periods to comments * reworked so that entities don't fire events * lint * review comments --- homeassistant/components/binary_sensor/zha.py | 8 +++ homeassistant/components/zha/__init__.py | 33 ++++++++- homeassistant/components/zha/const.py | 7 ++ .../components/zha/entities/entity.py | 5 ++ homeassistant/components/zha/event.py | 69 +++++++++++++++++++ 5 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/zha/event.py diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index 62c57f0288b..aa1f4eb2f86 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -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) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index fb909b6fedf..41659ae47df 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -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 diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 9efa847b50c..7da6f826c44 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -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', diff --git a/homeassistant/components/zha/entities/entity.py b/homeassistant/components/zha/entities/entity.py index da8f615a665..920c90a4cd1 100644 --- a/homeassistant/components/zha/entities/entity.py +++ b/homeassistant/components/zha/entities/entity.py @@ -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 diff --git a/homeassistant/components/zha/event.py b/homeassistant/components/zha/event.py new file mode 100644 index 00000000000..20175dd097f --- /dev/null +++ b/homeassistant/components/zha/event.py @@ -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 + )