Clean up ZHA post rewrite (#21448)

* update async handling to reduce unnecessary coroutine creation

* lint

* cleanup
This commit is contained in:
David F. Mulcahey 2019-02-26 13:48:10 -05:00 committed by Paulus Schoutsen
parent beb86426e4
commit a34524febe
6 changed files with 110 additions and 91 deletions

View File

@ -154,11 +154,9 @@ async def async_setup_entry(hass, config_entry):
"""Handle message from a device.""" """Handle message from a device."""
if not sender.initializing and sender.ieee in zha_gateway.devices and \ if not sender.initializing and sender.ieee in zha_gateway.devices and \
not zha_gateway.devices[sender.ieee].available: not zha_gateway.devices[sender.ieee].available:
hass.async_create_task( zha_gateway.async_device_became_available(
zha_gateway.async_device_became_available( sender, is_reply, profile, cluster, src_ep, dst_ep, tsn,
sender, is_reply, profile, cluster, src_ep, dst_ep, tsn, command_id, args
command_id, args
)
) )
return sender.handle_message( return sender.handle_message(
is_reply, profile, cluster, src_ep, dst_ep, tsn, command_id, args) is_reply, profile, cluster, src_ep, dst_ep, tsn, command_id, args)

View File

@ -251,7 +251,7 @@ def async_load_api(hass, application_controller, zha_gateway):
zha_device = zha_gateway.get_device(ieee) zha_device = zha_gateway.get_device(ieee)
response_clusters = [] response_clusters = []
if zha_device is not None: 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 ep_id, clusters in clusters_by_endpoint.items():
for c_id, cluster in clusters[IN].items(): for c_id, cluster in clusters[IN].items():
response_clusters.append({ response_clusters.append({
@ -289,7 +289,7 @@ def async_load_api(hass, application_controller, zha_gateway):
zha_device = zha_gateway.get_device(ieee) zha_device = zha_gateway.get_device(ieee)
attributes = None attributes = None
if zha_device is not None: if zha_device is not None:
attributes = await zha_device.get_cluster_attributes( attributes = zha_device.async_get_cluster_attributes(
endpoint_id, endpoint_id,
cluster_id, cluster_id,
cluster_type) cluster_type)
@ -329,7 +329,7 @@ def async_load_api(hass, application_controller, zha_gateway):
cluster_commands = [] cluster_commands = []
commands = None commands = None
if zha_device is not None: if zha_device is not None:
commands = await zha_device.get_cluster_commands( commands = zha_device.async_get_cluster_commands(
endpoint_id, endpoint_id,
cluster_id, cluster_id,
cluster_type) cluster_type)
@ -380,7 +380,7 @@ def async_load_api(hass, application_controller, zha_gateway):
zha_device = zha_gateway.get_device(ieee) zha_device = zha_gateway.get_device(ieee)
success = failure = None success = failure = None
if zha_device is not 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) endpoint_id, cluster_id, cluster_type=cluster_type)
success, failure = await cluster.read_attributes( success, failure = await cluster.read_attributes(
[attribute], [attribute],

View File

@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/ https://home-assistant.io/components/zha/
""" """
import asyncio import asyncio
from concurrent.futures import TimeoutError as Timeout
from enum import Enum from enum import Enum
from functools import wraps from functools import wraps
import logging import logging
@ -55,9 +56,13 @@ def decorate_command(channel, command):
if isinstance(result, bool): if isinstance(result, bool):
return result return result
return result[1] is Status.SUCCESS return result[1] is Status.SUCCESS
except DeliveryError: except (DeliveryError, Timeout) as ex:
_LOGGER.debug("%s: command failed: %s", channel.unique_id, _LOGGER.debug(
command.__name__) "%s: command failed: %s exception: %s",
channel.unique_id,
command.__name__,
str(ex)
)
return False return False
return wrapper return wrapper

View File

@ -28,8 +28,16 @@ class ColorChannel(ZigbeeChannel):
"""Return the color capabilities.""" """Return the color capabilities."""
return self._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): async def async_initialize(self, from_cache):
"""Initialize channel.""" """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( capabilities = await self.get_attribute_value(
'color_capabilities', from_cache=from_cache) 'color_capabilities', from_cache=from_cache)

View File

@ -8,6 +8,7 @@ import asyncio
from enum import Enum from enum import Enum
import logging import logging
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send async_dispatcher_connect, async_dispatcher_send
) )
@ -188,13 +189,14 @@ class ZHADevice:
"""Initialize channels.""" """Initialize channels."""
_LOGGER.debug('%s: started initialization', self.name) _LOGGER.debug('%s: started initialization', self.name)
await self._execute_channel_tasks('async_initialize', from_cache) await self._execute_channel_tasks('async_initialize', from_cache)
self.power_source = self.cluster_channels.get( if BASIC_CHANNEL in self.cluster_channels:
BASIC_CHANNEL).get_power_source() self.power_source = self.cluster_channels.get(
_LOGGER.debug( BASIC_CHANNEL).get_power_source()
'%s: power source: %s', _LOGGER.debug(
self.name, '%s: power source: %s',
BasicChannel.POWER_SOURCES.get(self.power_source) self.name,
) BasicChannel.POWER_SOURCES.get(self.power_source)
)
self.status = DeviceStatus.INITIALIZED self.status = DeviceStatus.INITIALIZED
_LOGGER.debug('%s: completed initialization', self.name) _LOGGER.debug('%s: completed initialization', self.name)
@ -229,7 +231,8 @@ class ZHADevice:
if self._unsub: if self._unsub:
self._unsub() self._unsub()
async def get_clusters(self): @callback
def async_get_clusters(self):
"""Get all clusters for this device.""" """Get all clusters for this device."""
return { return {
ep_id: { ep_id: {
@ -239,25 +242,27 @@ class ZHADevice:
if ep_id != 0 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.""" """Get zigbee cluster from this entity."""
clusters = await self.get_clusters() clusters = self.async_get_clusters()
return clusters[endpoint_id][cluster_type][cluster_id] 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): cluster_type=IN):
"""Get zigbee attributes for specified cluster.""" """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) cluster_type)
if cluster is None: if cluster is None:
return None return None
return cluster.attributes 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): cluster_type=IN):
"""Get zigbee commands for specified cluster.""" """Get zigbee commands for specified cluster."""
cluster = await self.get_cluster(endpoint_id, cluster_id, cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type)
cluster_type)
if cluster is None: if cluster is None:
return None return None
return { return {
@ -269,8 +274,7 @@ class ZHADevice:
attribute, value, cluster_type=IN, attribute, value, cluster_type=IN,
manufacturer=None): manufacturer=None):
"""Write a value to a zigbee attribute for a cluster in this entity.""" """Write a value to a zigbee attribute for a cluster in this entity."""
cluster = await self.get_cluster( cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type)
endpoint_id, cluster_id, cluster_type)
if cluster is None: if cluster is None:
return None return None
@ -304,8 +308,7 @@ class ZHADevice:
command_type, args, cluster_type=IN, command_type, args, cluster_type=IN,
manufacturer=None): manufacturer=None):
"""Issue a command against specified zigbee cluster on this entity.""" """Issue a command against specified zigbee cluster on this entity."""
cluster = await self.get_cluster( cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type)
endpoint_id, cluster_id, cluster_type)
if cluster is None: if cluster is None:
return None return None
response = None response = None

View File

@ -5,11 +5,11 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/ https://home-assistant.io/components/zha/
""" """
import asyncio
import collections import collections
import itertools import itertools
import logging import logging
from homeassistant import const as ha_const from homeassistant import const as ha_const
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from . import const as zha_const 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.""" """Get or create a ZHA device."""
zha_device = self._devices.get(zigpy_device.ieee) zha_device = self._devices.get(zigpy_device.ieee)
if zha_device is None: if zha_device is None:
@ -130,12 +131,14 @@ class ZHAGateway:
self._devices[zigpy_device.ieee] = zha_device self._devices[zigpy_device.ieee] = zha_device
return 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, self, sender, is_reply, profile, cluster, src_ep, dst_ep, tsn,
command_id, args): command_id, args):
"""Handle tasks when a device becomes available.""" """Handle tasks when a device becomes available."""
self.async_update_device(sender) self.async_update_device(sender)
@callback
def async_update_device(self, sender): def async_update_device(self, sender):
"""Update device that has just become available.""" """Update device that has just become available."""
if sender.ieee in self.devices: if sender.ieee in self.devices:
@ -146,34 +149,17 @@ class ZHAGateway:
async def async_device_initialized(self, device, is_new_join): async def async_device_initialized(self, device, is_new_join):
"""Handle device joined and basic information discovered (async).""" """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 = [] discovery_infos = []
endpoint_tasks = []
for endpoint_id, endpoint in device.endpoints.items(): 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, endpoint_id, endpoint, discovery_infos, device, zha_device,
is_new_join 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: if is_new_join:
# because it's a new join we can immediately mark the device as # configure the device
# available and we already loaded fresh state above await zha_device.async_configure()
zha_device.update_available(True)
elif not zha_device.available and zha_device.power_source is not None\ 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.BATTERY\
and zha_device.power_source != BasicChannel.UNKNOWN: and zha_device.power_source != BasicChannel.UNKNOWN:
@ -187,15 +173,33 @@ class ZHAGateway:
) )
) )
await zha_device.async_initialize(from_cache=False) 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, self, endpoint_id, endpoint, discovery_infos, device, zha_device,
is_new_join): is_new_join):
"""Process an endpoint on a zigpy device.""" """Process an endpoint on a zigpy device."""
import zigpy.profiles import zigpy.profiles
if endpoint_id == 0: # ZDO if endpoint_id == 0: # ZDO
await _create_cluster_channel( _async_create_cluster_channel(
endpoint, endpoint,
zha_device, zha_device,
is_new_join, is_new_join,
@ -226,12 +230,12 @@ class ZHAGateway:
profile_clusters = zha_const.COMPONENT_CLUSTERS[component] profile_clusters = zha_const.COMPONENT_CLUSTERS[component]
if component and component in COMPONENTS: 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, self._hass, endpoint, profile_clusters, zha_device,
component, device_key, is_new_join) component, device_key, is_new_join)
discovery_infos.append(profile_match) discovery_infos.append(profile_match)
discovery_infos.extend(await _handle_single_cluster_matches( discovery_infos.extend(_async_handle_single_cluster_matches(
self._hass, self._hass,
endpoint, endpoint,
zha_device, 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): channels=None, channel_class=None):
"""Create a cluster channel and attach it to a device.""" """Create a cluster channel and attach it to a device."""
if channel_class is None: if channel_class is None:
channel_class = ZIGBEE_CHANNEL_REGISTRY.get(cluster.cluster_id, channel_class = ZIGBEE_CHANNEL_REGISTRY.get(cluster.cluster_id,
AttributeListeningChannel) AttributeListeningChannel)
channel = channel_class(cluster, zha_device) channel = channel_class(cluster, zha_device)
if is_new_join:
await channel.async_configure()
zha_device.add_cluster_channel(channel) zha_device.add_cluster_channel(channel)
if channels is not None: if channels is not None:
channels.append(channel) 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.""" """Dispatch or store discovery information."""
if not discovery_info['channels']: if not discovery_info['channels']:
_LOGGER.warning( _LOGGER.warning(
@ -273,7 +277,8 @@ async def _dispatch_discovery_info(hass, is_new_join, discovery_info):
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): component, device_key, is_new_join):
"""Dispatch a profile match to the appropriate HA component.""" """Dispatch a profile match to the appropriate HA component."""
in_clusters = [endpoint.in_clusters[c] 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] if c in endpoint.out_clusters]
channels = [] channels = []
cluster_tasks = []
for cluster in in_clusters: for cluster in in_clusters:
cluster_tasks.append(_create_cluster_channel( _async_create_cluster_channel(
cluster, zha_device, is_new_join, channels=channels)) cluster, zha_device, is_new_join, channels=channels)
for cluster in out_clusters: for cluster in out_clusters:
cluster_tasks.append(_create_cluster_channel( _async_create_cluster_channel(
cluster, zha_device, is_new_join, channels=channels)) cluster, zha_device, is_new_join, channels=channels)
await asyncio.gather(*cluster_tasks)
discovery_info = { discovery_info = {
'unique_id': device_key, 'unique_id': device_key,
@ -319,24 +321,25 @@ async def _handle_profile_match(hass, endpoint, profile_clusters, zha_device,
return discovery_info 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, profile_clusters, device_key,
is_new_join): is_new_join):
"""Dispatch single cluster matches to HA components.""" """Dispatch single cluster matches to HA components."""
cluster_matches = [] cluster_matches = []
cluster_match_tasks = [] cluster_match_results = []
event_channel_tasks = []
for cluster in endpoint.in_clusters.values(): for cluster in endpoint.in_clusters.values():
# don't let profiles prevent these channels from being created # don't let profiles prevent these channels from being created
if cluster.cluster_id in NO_SENSOR_CLUSTERS: if cluster.cluster_id in NO_SENSOR_CLUSTERS:
cluster_match_tasks.append(_handle_channel_only_cluster_match( cluster_match_results.append(
zha_device, _async_handle_channel_only_cluster_match(
cluster, zha_device,
is_new_join, cluster,
)) is_new_join,
))
if cluster.cluster_id not in profile_clusters[0]: 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, hass,
zha_device, zha_device,
cluster, cluster,
@ -347,7 +350,7 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device,
for cluster in endpoint.out_clusters.values(): for cluster in endpoint.out_clusters.values():
if cluster.cluster_id not in profile_clusters[1]: 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, hass,
zha_device, zha_device,
cluster, cluster,
@ -357,27 +360,28 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device,
)) ))
if cluster.cluster_id in EVENT_RELAY_CLUSTERS: if cluster.cluster_id in EVENT_RELAY_CLUSTERS:
event_channel_tasks.append(_create_cluster_channel( _async_create_cluster_channel(
cluster, cluster,
zha_device, zha_device,
is_new_join, is_new_join,
channel_class=EventRelayChannel 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: for cluster_match in cluster_match_results:
if cluster_match is not None: if cluster_match is not None:
cluster_matches.append(cluster_match) cluster_matches.append(cluster_match)
return cluster_matches return cluster_matches
async def _handle_channel_only_cluster_match( @callback
def _async_handle_channel_only_cluster_match(
zha_device, cluster, is_new_join): zha_device, cluster, is_new_join):
"""Handle a channel only cluster match.""" """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): device_classes, is_new_join):
"""Dispatch a single cluster match to a HA component.""" """Dispatch a single cluster match to a HA component."""
component = None # sub_component = None 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: if component is None or component not in COMPONENTS:
return return
channels = [] channels = []
await _create_cluster_channel(cluster, zha_device, is_new_join, _async_create_cluster_channel(cluster, zha_device, is_new_join,
channels=channels) channels=channels)
cluster_key = "{}-{}".format(device_key, cluster.cluster_id) 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 return discovery_info
def _create_device_entity(zha_device): @callback
def _async_create_device_entity(zha_device):
"""Create ZHADeviceEntity.""" """Create ZHADeviceEntity."""
device_entity_channels = [] device_entity_channels = []
if POWER_CONFIGURATION_CHANNEL in zha_device.cluster_channels: if POWER_CONFIGURATION_CHANNEL in zha_device.cluster_channels: