Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ZHA - Event foundation #19095

Merged
merged 5 commits into from
Dec 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions homeassistant/components/binary_sensor/zha.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@ def zdo_command(self, *args, **kwargs):
"""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."""

Expand Down Expand Up @@ -220,6 +224,10 @@ def zdo_command(self, *args, **kwargs):
"""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)
Expand Down
33 changes: 32 additions & 1 deletion homeassistant/components/zha/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import collections
import logging
import os
import types

import voluptuous as vol

Expand All @@ -20,12 +21,14 @@
# 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',
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()

Expand All @@ -221,13 +240,15 @@ def __init__(self, hass, config):
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:
hass.data[DATA_ZHA][component] = (
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.
Expand Down Expand Up @@ -256,6 +277,8 @@ def device_removed(self, device):
"""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)."""
Expand Down Expand Up @@ -362,6 +385,14 @@ async def _attempt_single_cluster_device(self, endpoint, cluster,
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

Expand Down
7 changes: 7 additions & 0 deletions homeassistant/components/zha/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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 = [
Expand Down Expand Up @@ -53,6 +54,7 @@ def list(cls):
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {}
CUSTOM_CLUSTER_MAPPINGS = {}
COMPONENT_CLUSTERS = {}
EVENTABLE_CLUSTERS = []


def populate_data():
Expand All @@ -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',
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/zha/entities/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,8 @@ def device_info(self):
'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
69 changes: 69 additions & 0 deletions homeassistant/components/zha/event.py
Original file line number Diff line number Diff line change
@@ -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,
dmulcahey marked this conversation as resolved.
Show resolved Hide resolved
'args': args
},
EventOrigin.remote
)