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
Changes from all commits
8c69c15
c4af1e2
9076fe5
3b1e44a
d050473
e13745b
857c2a2
e3a4ff7
867ad95
d0194d4
3189973
0a0938b
b3a0020
f0a2d97
93f38e1
9d6f00c
96445b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
zha_const.populate_data(self._hass) | ||
|
||
def device_joined(self, device): | ||
"""Handle device joined. | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See above. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please move this too. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
||
|
@@ -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, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does it need to be configurable? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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', | ||
|
@@ -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', | ||
|
@@ -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] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.