ZHA - Event foundation (#19095)
* event foundation * add missing periods to comments * reworked so that entities don't fire events * lint * review comments
This commit is contained in:
parent
59581786d3
commit
f4f42176bd
5 changed files with 121 additions and 1 deletions
|
@ -190,6 +190,10 @@ class Remote(ZhaEntity, BinarySensorDevice):
|
||||||
"""Handle ZDO commands on this cluster."""
|
"""Handle ZDO commands on this cluster."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def zha_send_event(self, cluster, command, args):
|
||||||
|
"""Relay entity events to hass."""
|
||||||
|
pass # don't let entities fire events
|
||||||
|
|
||||||
class LevelListener:
|
class LevelListener:
|
||||||
"""Listener for the LevelControl Zigbee cluster."""
|
"""Listener for the LevelControl Zigbee cluster."""
|
||||||
|
|
||||||
|
@ -220,6 +224,10 @@ class Remote(ZhaEntity, BinarySensorDevice):
|
||||||
"""Handle ZDO commands on this cluster."""
|
"""Handle ZDO commands on this cluster."""
|
||||||
pass
|
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):
|
def __init__(self, **kwargs):
|
||||||
"""Initialize Switch."""
|
"""Initialize Switch."""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
|
@ -7,6 +7,7 @@ https://home-assistant.io/components/zha/
|
||||||
import collections
|
import collections
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import types
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
@ -20,12 +21,14 @@ from homeassistant.helpers.entity_component import EntityComponent
|
||||||
# Loading the config flow file will register the flow
|
# Loading the config flow file will register the flow
|
||||||
from . import config_flow # noqa # pylint: disable=unused-import
|
from . import config_flow # noqa # pylint: disable=unused-import
|
||||||
from . import const as zha_const
|
from . import const as zha_const
|
||||||
|
from .event import ZhaEvent
|
||||||
from .const import (
|
from .const import (
|
||||||
COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG,
|
COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG,
|
||||||
CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, DATA_ZHA_BRIDGE_ID,
|
CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, DATA_ZHA_BRIDGE_ID,
|
||||||
DATA_ZHA_CONFIG, DATA_ZHA_CORE_COMPONENT, DATA_ZHA_DISPATCHERS,
|
DATA_ZHA_CONFIG, DATA_ZHA_CORE_COMPONENT, DATA_ZHA_DISPATCHERS,
|
||||||
DATA_ZHA_RADIO, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME,
|
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 = [
|
REQUIREMENTS = [
|
||||||
'bellows==0.7.0',
|
'bellows==0.7.0',
|
||||||
|
@ -130,6 +133,19 @@ async def async_setup_entry(hass, config_entry):
|
||||||
database = config[CONF_DATABASE]
|
database = config[CONF_DATABASE]
|
||||||
else:
|
else:
|
||||||
database = os.path.join(hass.config.config_dir, DEFAULT_DATABASE_NAME)
|
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)
|
APPLICATION_CONTROLLER = ControllerApplication(radio, database)
|
||||||
listener = ApplicationListener(hass, config)
|
listener = ApplicationListener(hass, config)
|
||||||
APPLICATION_CONTROLLER.add_listener(listener)
|
APPLICATION_CONTROLLER.add_listener(listener)
|
||||||
|
@ -205,6 +221,9 @@ async def async_unload_entry(hass, config_entry):
|
||||||
for entity_id in entity_ids:
|
for entity_id in entity_ids:
|
||||||
await component.async_remove_entity(entity_id)
|
await component.async_remove_entity(entity_id)
|
||||||
|
|
||||||
|
# clean up events
|
||||||
|
hass.data[DATA_ZHA][DATA_ZHA_CORE_EVENTS].clear()
|
||||||
|
|
||||||
_LOGGER.debug("Closing zha radio")
|
_LOGGER.debug("Closing zha radio")
|
||||||
hass.data[DATA_ZHA][DATA_ZHA_RADIO].close()
|
hass.data[DATA_ZHA][DATA_ZHA_RADIO].close()
|
||||||
|
|
||||||
|
@ -221,6 +240,7 @@ class ApplicationListener:
|
||||||
self._config = config
|
self._config = config
|
||||||
self._component = EntityComponent(_LOGGER, DOMAIN, hass)
|
self._component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||||
self._device_registry = collections.defaultdict(list)
|
self._device_registry = collections.defaultdict(list)
|
||||||
|
self._events = {}
|
||||||
zha_const.populate_data()
|
zha_const.populate_data()
|
||||||
|
|
||||||
for component in COMPONENTS:
|
for component in COMPONENTS:
|
||||||
|
@ -228,6 +248,7 @@ class ApplicationListener:
|
||||||
hass.data[DATA_ZHA].get(component, {})
|
hass.data[DATA_ZHA].get(component, {})
|
||||||
)
|
)
|
||||||
hass.data[DATA_ZHA][DATA_ZHA_CORE_COMPONENT] = self._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):
|
def device_joined(self, device):
|
||||||
"""Handle device joined.
|
"""Handle device joined.
|
||||||
|
@ -256,6 +277,8 @@ class ApplicationListener:
|
||||||
"""Handle device being removed from the network."""
|
"""Handle device being removed from the network."""
|
||||||
for device_entity in self._device_registry[device.ieee]:
|
for device_entity in self._device_registry[device.ieee]:
|
||||||
self._hass.async_create_task(device_entity.async_remove())
|
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):
|
async def async_device_initialized(self, device, join):
|
||||||
"""Handle device joined and basic information discovered (async)."""
|
"""Handle device joined and basic information discovered (async)."""
|
||||||
|
@ -362,6 +385,14 @@ class ApplicationListener:
|
||||||
device_classes, discovery_attr,
|
device_classes, discovery_attr,
|
||||||
is_new_join):
|
is_new_join):
|
||||||
"""Try to set up an entity from a "bare" cluster."""
|
"""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:
|
if cluster.cluster_id in profile_clusters:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ DATA_ZHA_BRIDGE_ID = 'zha_bridge_id'
|
||||||
DATA_ZHA_RADIO = 'zha_radio'
|
DATA_ZHA_RADIO = 'zha_radio'
|
||||||
DATA_ZHA_DISPATCHERS = 'zha_dispatchers'
|
DATA_ZHA_DISPATCHERS = 'zha_dispatchers'
|
||||||
DATA_ZHA_CORE_COMPONENT = 'zha_core_component'
|
DATA_ZHA_CORE_COMPONENT = 'zha_core_component'
|
||||||
|
DATA_ZHA_CORE_EVENTS = 'zha_core_events'
|
||||||
ZHA_DISCOVERY_NEW = 'zha_discovery_new_{}'
|
ZHA_DISCOVERY_NEW = 'zha_discovery_new_{}'
|
||||||
|
|
||||||
COMPONENTS = [
|
COMPONENTS = [
|
||||||
|
@ -53,6 +54,7 @@ SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {}
|
||||||
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {}
|
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {}
|
||||||
CUSTOM_CLUSTER_MAPPINGS = {}
|
CUSTOM_CLUSTER_MAPPINGS = {}
|
||||||
COMPONENT_CLUSTERS = {}
|
COMPONENT_CLUSTERS = {}
|
||||||
|
EVENTABLE_CLUSTERS = []
|
||||||
|
|
||||||
|
|
||||||
def populate_data():
|
def populate_data():
|
||||||
|
@ -70,6 +72,11 @@ def populate_data():
|
||||||
if zll.PROFILE_ID not in DEVICE_CLASS:
|
if zll.PROFILE_ID not in DEVICE_CLASS:
|
||||||
DEVICE_CLASS[zll.PROFILE_ID] = {}
|
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({
|
DEVICE_CLASS[zha.PROFILE_ID].update({
|
||||||
zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor',
|
zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor',
|
||||||
zha.DeviceType.LEVEL_CONTROL_SWITCH: 'binary_sensor',
|
zha.DeviceType.LEVEL_CONTROL_SWITCH: 'binary_sensor',
|
||||||
|
|
|
@ -103,3 +103,8 @@ class ZhaEntity(entity.Entity):
|
||||||
'name': self._device_state_attributes['friendly_name'],
|
'name': self._device_state_attributes['friendly_name'],
|
||||||
'via_hub': (DOMAIN, self.hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID]),
|
'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
homeassistant/components/zha/event.py
Normal file
69
homeassistant/components/zha/event.py
Normal 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
|
||||||
|
)
|
Loading…
Add table
Reference in a new issue