diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index a53e5864552..96c3a30d313 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -154,11 +154,9 @@ async def async_setup_entry(hass, config_entry): """Handle message from a device.""" if not sender.initializing and sender.ieee in zha_gateway.devices and \ not zha_gateway.devices[sender.ieee].available: - hass.async_create_task( - zha_gateway.async_device_became_available( - sender, is_reply, profile, cluster, src_ep, dst_ep, tsn, - command_id, args - ) + zha_gateway.async_device_became_available( + sender, is_reply, profile, cluster, src_ep, dst_ep, tsn, + command_id, args ) return sender.handle_message( is_reply, profile, cluster, src_ep, dst_ep, tsn, command_id, args) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 0dd6dd78400..f0739f9a073 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -251,7 +251,7 @@ def async_load_api(hass, application_controller, zha_gateway): zha_device = zha_gateway.get_device(ieee) response_clusters = [] if zha_device is not None: - clusters_by_endpoint = await zha_device.get_clusters() + clusters_by_endpoint = zha_device.async_get_clusters() for ep_id, clusters in clusters_by_endpoint.items(): for c_id, cluster in clusters[IN].items(): response_clusters.append({ @@ -289,7 +289,7 @@ def async_load_api(hass, application_controller, zha_gateway): zha_device = zha_gateway.get_device(ieee) attributes = None if zha_device is not None: - attributes = await zha_device.get_cluster_attributes( + attributes = zha_device.async_get_cluster_attributes( endpoint_id, cluster_id, cluster_type) @@ -329,7 +329,7 @@ def async_load_api(hass, application_controller, zha_gateway): cluster_commands = [] commands = None if zha_device is not None: - commands = await zha_device.get_cluster_commands( + commands = zha_device.async_get_cluster_commands( endpoint_id, cluster_id, cluster_type) @@ -380,7 +380,7 @@ def async_load_api(hass, application_controller, zha_gateway): zha_device = zha_gateway.get_device(ieee) success = failure = None if zha_device is not None: - cluster = await zha_device.get_cluster( + cluster = zha_device.async_get_cluster( endpoint_id, cluster_id, cluster_type=cluster_type) success, failure = await cluster.read_attributes( [attribute], diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 0c0e1ed2173..a070343b775 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ import asyncio +from concurrent.futures import TimeoutError as Timeout from enum import Enum from functools import wraps import logging @@ -55,9 +56,13 @@ def decorate_command(channel, command): if isinstance(result, bool): return result return result[1] is Status.SUCCESS - except DeliveryError: - _LOGGER.debug("%s: command failed: %s", channel.unique_id, - command.__name__) + except (DeliveryError, Timeout) as ex: + _LOGGER.debug( + "%s: command failed: %s exception: %s", + channel.unique_id, + command.__name__, + str(ex) + ) return False return wrapper diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index ee88a30e828..9c904a7a001 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -28,8 +28,16 @@ class ColorChannel(ZigbeeChannel): """Return the color capabilities.""" return self._color_capabilities + async def async_configure(self): + """Configure channel.""" + await self.fetch_color_capabilities(False) + async def async_initialize(self, from_cache): """Initialize channel.""" + await self.fetch_color_capabilities(True) + + async def fetch_color_capabilities(self, from_cache): + """Get the color configuration.""" capabilities = await self.get_attribute_value( 'color_capabilities', from_cache=from_cache) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 1ee800d8559..102c9bed2d3 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -8,6 +8,7 @@ import asyncio from enum import Enum import logging +from homeassistant.core import callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send ) @@ -188,13 +189,14 @@ class ZHADevice: """Initialize channels.""" _LOGGER.debug('%s: started initialization', self.name) await self._execute_channel_tasks('async_initialize', from_cache) - self.power_source = self.cluster_channels.get( - BASIC_CHANNEL).get_power_source() - _LOGGER.debug( - '%s: power source: %s', - self.name, - BasicChannel.POWER_SOURCES.get(self.power_source) - ) + if BASIC_CHANNEL in self.cluster_channels: + self.power_source = self.cluster_channels.get( + BASIC_CHANNEL).get_power_source() + _LOGGER.debug( + '%s: power source: %s', + self.name, + BasicChannel.POWER_SOURCES.get(self.power_source) + ) self.status = DeviceStatus.INITIALIZED _LOGGER.debug('%s: completed initialization', self.name) @@ -229,7 +231,8 @@ class ZHADevice: if self._unsub: self._unsub() - async def get_clusters(self): + @callback + def async_get_clusters(self): """Get all clusters for this device.""" return { ep_id: { @@ -239,25 +242,27 @@ class ZHADevice: if ep_id != 0 } - async def get_cluster(self, endpoint_id, cluster_id, cluster_type=IN): + @callback + def async_get_cluster(self, endpoint_id, cluster_id, cluster_type=IN): """Get zigbee cluster from this entity.""" - clusters = await self.get_clusters() + clusters = self.async_get_clusters() return clusters[endpoint_id][cluster_type][cluster_id] - async def get_cluster_attributes(self, endpoint_id, cluster_id, + @callback + def async_get_cluster_attributes(self, endpoint_id, cluster_id, cluster_type=IN): """Get zigbee attributes for specified cluster.""" - cluster = await self.get_cluster(endpoint_id, cluster_id, + cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) if cluster is None: return None return cluster.attributes - async def get_cluster_commands(self, endpoint_id, cluster_id, + @callback + def async_get_cluster_commands(self, endpoint_id, cluster_id, cluster_type=IN): """Get zigbee commands for specified cluster.""" - cluster = await self.get_cluster(endpoint_id, cluster_id, - cluster_type) + cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) if cluster is None: return None return { @@ -269,8 +274,7 @@ class ZHADevice: attribute, value, cluster_type=IN, manufacturer=None): """Write a value to a zigbee attribute for a cluster in this entity.""" - cluster = await self.get_cluster( - endpoint_id, cluster_id, cluster_type) + cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) if cluster is None: return None @@ -304,8 +308,7 @@ class ZHADevice: command_type, args, cluster_type=IN, manufacturer=None): """Issue a command against specified zigbee cluster on this entity.""" - cluster = await self.get_cluster( - endpoint_id, cluster_id, cluster_type) + cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) if cluster is None: return None response = None diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index cb5e5bf7774..563543fa4bd 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -5,11 +5,11 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ -import asyncio import collections import itertools import logging from homeassistant import const as ha_const +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_component import EntityComponent from . import const as zha_const @@ -122,7 +122,8 @@ class ZHAGateway: ) ) - async def _get_or_create_device(self, zigpy_device): + @callback + def _async_get_or_create_device(self, zigpy_device): """Get or create a ZHA device.""" zha_device = self._devices.get(zigpy_device.ieee) if zha_device is None: @@ -130,12 +131,14 @@ class ZHAGateway: self._devices[zigpy_device.ieee] = zha_device return zha_device - async def async_device_became_available( + @callback + def async_device_became_available( self, sender, is_reply, profile, cluster, src_ep, dst_ep, tsn, command_id, args): """Handle tasks when a device becomes available.""" self.async_update_device(sender) + @callback def async_update_device(self, sender): """Update device that has just become available.""" if sender.ieee in self.devices: @@ -146,34 +149,17 @@ class ZHAGateway: async def async_device_initialized(self, device, is_new_join): """Handle device joined and basic information discovered (async).""" - zha_device = await self._get_or_create_device(device) + zha_device = self._async_get_or_create_device(device) discovery_infos = [] - endpoint_tasks = [] for endpoint_id, endpoint in device.endpoints.items(): - endpoint_tasks.append(self._async_process_endpoint( + self._async_process_endpoint( endpoint_id, endpoint, discovery_infos, device, zha_device, is_new_join - )) - await asyncio.gather(*endpoint_tasks) - - await zha_device.async_initialize(from_cache=(not is_new_join)) - - discovery_tasks = [] - for discovery_info in discovery_infos: - discovery_tasks.append(_dispatch_discovery_info( - self._hass, - is_new_join, - discovery_info - )) - await asyncio.gather(*discovery_tasks) - - device_entity = _create_device_entity(zha_device) - await self._component.async_add_entities([device_entity]) + ) if is_new_join: - # because it's a new join we can immediately mark the device as - # available and we already loaded fresh state above - zha_device.update_available(True) + # configure the device + await zha_device.async_configure() elif not zha_device.available and zha_device.power_source is not None\ and zha_device.power_source != BasicChannel.BATTERY\ and zha_device.power_source != BasicChannel.UNKNOWN: @@ -187,15 +173,33 @@ class ZHAGateway: ) ) await zha_device.async_initialize(from_cache=False) + else: + await zha_device.async_initialize(from_cache=True) - async def _async_process_endpoint( + for discovery_info in discovery_infos: + _async_dispatch_discovery_info( + self._hass, + is_new_join, + discovery_info + ) + + device_entity = _async_create_device_entity(zha_device) + await self._component.async_add_entities([device_entity]) + + if is_new_join: + # because it's a new join we can immediately mark the device as + # available. We do it here because the entities didn't exist above + zha_device.update_available(True) + + @callback + def _async_process_endpoint( self, endpoint_id, endpoint, discovery_infos, device, zha_device, is_new_join): """Process an endpoint on a zigpy device.""" import zigpy.profiles if endpoint_id == 0: # ZDO - await _create_cluster_channel( + _async_create_cluster_channel( endpoint, zha_device, is_new_join, @@ -226,12 +230,12 @@ class ZHAGateway: profile_clusters = zha_const.COMPONENT_CLUSTERS[component] if component and component in COMPONENTS: - profile_match = await _handle_profile_match( + profile_match = _async_handle_profile_match( self._hass, endpoint, profile_clusters, zha_device, component, device_key, is_new_join) discovery_infos.append(profile_match) - discovery_infos.extend(await _handle_single_cluster_matches( + discovery_infos.extend(_async_handle_single_cluster_matches( self._hass, endpoint, zha_device, @@ -241,21 +245,21 @@ class ZHAGateway: )) -async def _create_cluster_channel(cluster, zha_device, is_new_join, +@callback +def _async_create_cluster_channel(cluster, zha_device, is_new_join, channels=None, channel_class=None): """Create a cluster channel and attach it to a device.""" if channel_class is None: channel_class = ZIGBEE_CHANNEL_REGISTRY.get(cluster.cluster_id, AttributeListeningChannel) channel = channel_class(cluster, zha_device) - if is_new_join: - await channel.async_configure() zha_device.add_cluster_channel(channel) if channels is not None: channels.append(channel) -async def _dispatch_discovery_info(hass, is_new_join, discovery_info): +@callback +def _async_dispatch_discovery_info(hass, is_new_join, discovery_info): """Dispatch or store discovery information.""" if not discovery_info['channels']: _LOGGER.warning( @@ -273,7 +277,8 @@ async def _dispatch_discovery_info(hass, is_new_join, discovery_info): discovery_info -async def _handle_profile_match(hass, endpoint, profile_clusters, zha_device, +@callback +def _async_handle_profile_match(hass, endpoint, profile_clusters, zha_device, component, device_key, is_new_join): """Dispatch a profile match to the appropriate HA component.""" in_clusters = [endpoint.in_clusters[c] @@ -284,17 +289,14 @@ async def _handle_profile_match(hass, endpoint, profile_clusters, zha_device, if c in endpoint.out_clusters] channels = [] - cluster_tasks = [] for cluster in in_clusters: - cluster_tasks.append(_create_cluster_channel( - cluster, zha_device, is_new_join, channels=channels)) + _async_create_cluster_channel( + cluster, zha_device, is_new_join, channels=channels) for cluster in out_clusters: - cluster_tasks.append(_create_cluster_channel( - cluster, zha_device, is_new_join, channels=channels)) - - await asyncio.gather(*cluster_tasks) + _async_create_cluster_channel( + cluster, zha_device, is_new_join, channels=channels) discovery_info = { 'unique_id': device_key, @@ -319,24 +321,25 @@ async def _handle_profile_match(hass, endpoint, profile_clusters, zha_device, return discovery_info -async def _handle_single_cluster_matches(hass, endpoint, zha_device, +@callback +def _async_handle_single_cluster_matches(hass, endpoint, zha_device, profile_clusters, device_key, is_new_join): """Dispatch single cluster matches to HA components.""" cluster_matches = [] - cluster_match_tasks = [] - event_channel_tasks = [] + cluster_match_results = [] for cluster in endpoint.in_clusters.values(): # don't let profiles prevent these channels from being created if cluster.cluster_id in NO_SENSOR_CLUSTERS: - cluster_match_tasks.append(_handle_channel_only_cluster_match( - zha_device, - cluster, - is_new_join, - )) + cluster_match_results.append( + _async_handle_channel_only_cluster_match( + zha_device, + cluster, + is_new_join, + )) if cluster.cluster_id not in profile_clusters[0]: - cluster_match_tasks.append(_handle_single_cluster_match( + cluster_match_results.append(_async_handle_single_cluster_match( hass, zha_device, cluster, @@ -347,7 +350,7 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device, for cluster in endpoint.out_clusters.values(): if cluster.cluster_id not in profile_clusters[1]: - cluster_match_tasks.append(_handle_single_cluster_match( + cluster_match_results.append(_async_handle_single_cluster_match( hass, zha_device, cluster, @@ -357,27 +360,28 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device, )) if cluster.cluster_id in EVENT_RELAY_CLUSTERS: - event_channel_tasks.append(_create_cluster_channel( + _async_create_cluster_channel( cluster, zha_device, is_new_join, channel_class=EventRelayChannel - )) - await asyncio.gather(*event_channel_tasks) - cluster_match_results = await asyncio.gather(*cluster_match_tasks) + ) + for cluster_match in cluster_match_results: if cluster_match is not None: cluster_matches.append(cluster_match) return cluster_matches -async def _handle_channel_only_cluster_match( +@callback +def _async_handle_channel_only_cluster_match( zha_device, cluster, is_new_join): """Handle a channel only cluster match.""" - await _create_cluster_channel(cluster, zha_device, is_new_join) + _async_create_cluster_channel(cluster, zha_device, is_new_join) -async def _handle_single_cluster_match(hass, zha_device, cluster, device_key, +@callback +def _async_handle_single_cluster_match(hass, zha_device, cluster, device_key, device_classes, is_new_join): """Dispatch a single cluster match to a HA component.""" component = None # sub_component = None @@ -392,7 +396,7 @@ async def _handle_single_cluster_match(hass, zha_device, cluster, device_key, if component is None or component not in COMPONENTS: return channels = [] - await _create_cluster_channel(cluster, zha_device, is_new_join, + _async_create_cluster_channel(cluster, zha_device, is_new_join, channels=channels) cluster_key = "{}-{}".format(device_key, cluster.cluster_id) @@ -416,7 +420,8 @@ async def _handle_single_cluster_match(hass, zha_device, cluster, device_key, return discovery_info -def _create_device_entity(zha_device): +@callback +def _async_create_device_entity(zha_device): """Create ZHADeviceEntity.""" device_entity_channels = [] if POWER_CONFIGURATION_CHANNEL in zha_device.cluster_channels: