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 - add events for remote like devices #18493

Closed
wants to merge 17 commits into from
45 changes: 44 additions & 1 deletion homeassistant/components/zha/__init__.py
Expand Up @@ -144,7 +144,10 @@ def __init__(self, hass, config):
self._config = config
self._component = EntityComponent(_LOGGER, DOMAIN, hass)
self._device_registry = collections.defaultdict(list)
self._events = []
hass.data[DISCOVERY_KEY] = hass.data.get(DISCOVERY_KEY, {})
import homeassistant.components.zha.const as zha_const
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be imported at the top of the module.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a design issue w/ the modules that causes circular imports. I have it corrected in another branch and can fix this in a subsequent PR if that is ok.

zha_const.populate_data(self._hass)

def device_joined(self, device):
"""Handle device joined.
Expand Down Expand Up @@ -177,8 +180,8 @@ def device_removed(self, device):
async def async_device_initialized(self, device, join):
"""Handle device joined and basic information discovered (async)."""
import zigpy.profiles
import homeassistant.components.zha.event as zha_event
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a design issue w/ the modules that causes circular imports. I have it corrected in another branch and can fix this in a subsequent PR if that is ok.

import homeassistant.components.zha.const as zha_const
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a design issue w/ the modules that causes circular imports. I have it corrected in another branch and can fix this in a subsequent PR if that is ok.

zha_const.populate_data()

device_manufacturer = device_model = None

Expand All @@ -197,6 +200,46 @@ async def async_device_initialized(self, device, join):
node_config = self._config[DOMAIN][CONF_DEVICE_CONFIG].get(
device_key, {})

_LOGGER.debug(
"Manufacturer: %s model: %s",
endpoint.manufacturer,
endpoint.model
)

if endpoint.profile_id in zigpy.profiles.PROFILES:
supported_remote_models = zha_const.REMOTE_DEVICE_TYPES.get(
endpoint.profile_id, {}).get(endpoint.manufacturer, [])

_LOGGER.debug(
"Supported remote models: %s",
supported_remote_models
)

if endpoint.model in supported_remote_models:
profile = zigpy.profiles.PROFILES[endpoint.profile_id]
profile_clusters = profile.CLUSTERS[endpoint.device_type]
in_clusters = [endpoint.in_clusters[c]
for c in profile_clusters[0]
if c in endpoint.in_clusters]
out_clusters = [endpoint.out_clusters[c]
for c in profile_clusters[1]
if c in endpoint.out_clusters]
discovery_info = {
'application_listener': self,
'endpoint': endpoint,
'in_clusters': in_clusters,
'out_clusters': out_clusters,
'manufacturer': endpoint.manufacturer,
'model': endpoint.model,
'new_join': join,
'unique_id': device_key,
}
created_events = await zha_event.async_setup_event(
self._hass,
discovery_info
)
self._events.extend(created_events)

if endpoint.profile_id in zigpy.profiles.PROFILES:
profile = zigpy.profiles.PROFILES[endpoint.profile_id]
if zha_const.DEVICE_CLASS.get(endpoint.profile_id,
Expand Down
42 changes: 40 additions & 2 deletions homeassistant/components/zha/const.py
@@ -1,13 +1,22 @@
"""All constants related to the ZHA component."""

import os
import logging

REMOTES_CONFIG_FILE = 'zha-remotes.json'
ZHA = 'Zigbee_home_automation'
ZLL = 'Zigbee_light_link'
DEVICE_CLASS = {}
SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {}
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {}
CUSTOM_CLUSTER_MAPPINGS = {}
COMPONENT_CLUSTERS = {}
REMOTE_DEVICE_TYPES = {}

_LOGGER = logging.getLogger(__name__)


def populate_data():
def populate_data(hass):
"""Populate data using constants from bellows.

These cannot be module level, as importing bellows must be done in a
Expand All @@ -16,6 +25,28 @@ def populate_data():
from zigpy import zcl, quirks
from zigpy.profiles import PROFILES, zha, zll
from homeassistant.components.sensor import zha as sensor_zha
from homeassistant.util.json import load_json, save_json

remotes_config_path = hass.config.path(REMOTES_CONFIG_FILE)

_LOGGER.debug(
"remotes config path: %s Is path: %s",
remotes_config_path,
os.path.isfile(remotes_config_path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is doing I/O in an async context. That's not allowed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any suggestions on how to handle? Should I just load this at the top of the module and not in the function?

)

if not os.path.isfile(remotes_config_path):
save_json(remotes_config_path,
{ZHA: {}, ZLL: {}}
)

remote_devices = load_json(remotes_config_path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are we doing here and what does it have to do with events? Looks like another feature, so should not be part of this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without this nothing sends events. This is a registry so users can opt certain devices in to sending events. It is described in the PR description.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does it need to be configurable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 100’s of zigbee devices and they don’t all follow defined specs. Also, there is no hard rule for what devices should use / need events. The alternative is a HA PR for every particular device that people want events from.

REMOTE_DEVICE_TYPES[zha.PROFILE_ID] = remote_devices.get(ZHA)
REMOTE_DEVICE_TYPES[zll.PROFILE_ID] = remote_devices.get(ZLL)

_LOGGER.debug(
"loaded from remotes config: %s", REMOTE_DEVICE_TYPES
)

DEVICE_CLASS[zha.PROFILE_ID] = {
zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor',
Expand Down Expand Up @@ -44,7 +75,6 @@ def populate_data():
zll.DeviceType.SCENE_CONTROLLER: 'binary_sensor',
zll.DeviceType.ON_OFF_SENSOR: 'binary_sensor',
}

SINGLE_INPUT_CLUSTER_DEVICE_CLASS.update({
zcl.clusters.general.OnOff: 'switch',
zcl.clusters.general.LevelControl: 'light',
Expand All @@ -67,6 +97,14 @@ def populate_data():
('sensor', sensor_zha.RelativeHumiditySensor)
})

# This registers a device that Xiaomi didn't follow the spec on.
# Translated: For device type: 0x5F01 in the ZHA zigbee profile
# the input clusters are: [0x0000, 0x0006, 0xFFFF] and the output
# clusters are: [0x0000, 0x0004, 0xFFFF]. The goal is to read this
# from a configuration file in the future
PROFILES[zha.PROFILE_ID].CLUSTERS[0x5F01] = ([0x0000, 0x0006, 0xFFFF],
[0x0000, 0x0004, 0xFFFF])

# A map of hass components to all Zigbee clusters it could use
for profile_id, classes in DEVICE_CLASS.items():
profile = PROFILES[profile_id]
Expand Down
97 changes: 97 additions & 0 deletions homeassistant/components/zha/event.py
@@ -0,0 +1,97 @@
"""
Support for Zigbee Home Automation devices that should fire events.

For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/
"""
import logging
from homeassistant.util import slugify
from homeassistant.core import EventOrigin, callback


_LOGGER = logging.getLogger(__name__)


async def async_setup_event(hass, discovery_info):
"""Set up events for devices that have been registered in const.py.

Will create events for devices registered in REMOTE_DEVICE_TYPES.
"""
from homeassistant.components.zha import configure_reporting
out_clusters = discovery_info['out_clusters']
in_clusters = discovery_info['in_clusters']
events = []
for in_cluster in in_clusters:
event = ZHAEvent(hass, in_cluster, discovery_info)
if discovery_info['new_join']:
await configure_reporting(event.event_id, in_cluster, 0,
False, 0, 600, 1)
events.append(event)
for out_cluster in out_clusters:
event = ZHAEvent(hass, out_cluster, discovery_info)
if discovery_info['new_join']:
await configure_reporting(event.event_id, out_cluster, 0,
False, 0, 600, 1)
events.append(event)
return events


class ZHAEvent():
"""When you want signals instead of entities.

Stateless sensors such as remotes are expected to generate an event
instead of a sensor entity in hass.
"""

def __init__(self, hass, cluster, discovery_info):
"""Register callback that will be used for signals."""
self._hass = hass
self._cluster = cluster
self._cluster.add_listener(self)
ieee = discovery_info['endpoint'].device.ieee
ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
if discovery_info['manufacturer'] and discovery_info['model'] is not \
None:
self.event_id = "{}.{}_{}_{}{}".format(
slugify(discovery_info['manufacturer']),
slugify(discovery_info['model']),
ieeetail,
discovery_info['endpoint'].endpoint_id,
discovery_info.get('entity_suffix', '')
)
else:
self.event_id = "{}.event_{}{}".format(
ieeetail,
discovery_info['endpoint'].endpoint_id,
discovery_info.get('entity_suffix', '')
)

@callback
def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster."""
self._hass.bus.async_fire(
'zha_' + self._cluster.server_commands.get(command_id)[0],
{'device': self.event_id, 'args': args},
EventOrigin.remote
)

@callback
def attribute_updated(self, attrid, value):
"""Handle attribute updates."""
self._hass.bus.async_fire(
'zha_attribute_updated',
{'device': self.event_id,
'attribute': self._cluster.attributes.get(attrid, ['Unknown'])[0],
'attribute_id': attrid,
'value': value},
EventOrigin.remote
)

@callback
def zdo_command(self, *args, **kwargs):
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
"""Log zdo commands for debugging."""
_LOGGER.debug(
"%s: issued zdo command %s with args: %s", self.event_id,
args,
kwargs
)