ZHA device channel refactoring (#31971)

* Add ZHA core typing helper.
* Add aux_channels to ZHA rule matching.
* Add match rule claim_channels() method.
* Expose underlying zigpy device.
* Not sure we need this one.
* Move "base" channels.
* Framework for channel discovery.
* Make DEVICE_CLASS and REMOTE_DEVICE_TYPE default dicts.
* Remove attribute reporting configuration registry.
* Refactor channels.
- Refactor zha events
- Use compound IDs and unique_ids
- Refactor signal dispatching on attribute updates

* Use unique id compatible with entities unique ids.
* Refactor ZHA Entity registry.
Let match rule to check for the match.

* Refactor discovery to use new channels.
* Cleanup ZDO channel.
Remove unused zha store call.

* Handle channel configuration and initialization.
* Refactor ZHA Device to use new channels.
* Refactor ZHA Gateway to use new discovery framework.
Use hass.data for entity info intermediate store.

* Don't keep entities in hass.data.
* ZHA gateway new discovery framework.
* Refactor ZHA platform loading.
* Don't update ZHA entities, when restoring from zigpy.
* ZHA entity discover tests.
* Add AnalogInput sensor.
* Remove 0xFC02 based entity from Keen smart vents.
* Clean up IAS channels.
* Refactor entity restoration.
* Fix lumi.router entities name.
* Rename EndpointsChannel to ChannelPool.
* Make Channels.pools a list.
* Fix cover test.
* Fix FakeDevice class.
* Fix device actions.
* Fix channels typing.
* Revert update_before_add=False
* Refactor channel class matching.
* Use a helper function for adding entities.
* Make Pylint happy.
* Rebase cleanup.
* Update coverage for ZHA device type overrides.
* Use cluster_id for single output cluster registry.
* Remove ZHA typing from coverage.
* Fix tests.
* Address comments.
* Address comments.
This commit is contained in:
Alexei Chetroi 2020-02-21 18:06:57 -05:00 committed by GitHub
parent 36db302cc8
commit 3385893b77
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 1918 additions and 1325 deletions

View file

@ -18,8 +18,9 @@ from homeassistant.helpers.dispatcher import (
async_dispatcher_send,
)
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import HomeAssistantType
from .channels import EventRelayChannel
from . import channels, typing as zha_typing
from .const import (
ATTR_ARGS,
ATTR_ATTRIBUTE,
@ -42,9 +43,6 @@ from .const import (
ATTR_QUIRK_CLASS,
ATTR_RSSI,
ATTR_VALUE,
CHANNEL_BASIC,
CHANNEL_POWER_CONFIGURATION,
CHANNEL_ZDO,
CLUSTER_COMMAND_SERVER,
CLUSTER_COMMANDS_CLIENT,
CLUSTER_COMMANDS_SERVER,
@ -75,14 +73,16 @@ class DeviceStatus(Enum):
class ZHADevice(LogMixin):
"""ZHA Zigbee device object."""
def __init__(self, hass, zigpy_device, zha_gateway):
def __init__(
self,
hass: HomeAssistantType,
zigpy_device: zha_typing.ZigpyDeviceType,
zha_gateway: zha_typing.ZhaGatewayType,
):
"""Initialize the gateway."""
self.hass = hass
self._zigpy_device = zigpy_device
self._zha_gateway = zha_gateway
self.cluster_channels = {}
self._relay_channels = {}
self._all_channels = []
self._available = False
self._available_signal = "{}_{}_{}".format(
self.name, self.ieee, SIGNAL_AVAILABLE
@ -101,6 +101,7 @@ class ZHADevice(LogMixin):
)
self._ha_device_id = None
self.status = DeviceStatus.CREATED
self._channels = channels.Channels(self)
@property
def device_id(self):
@ -111,6 +112,22 @@ class ZHADevice(LogMixin):
"""Set the HA device registry device id."""
self._ha_device_id = device_id
@property
def device(self) -> zha_typing.ZigpyDeviceType:
"""Return underlying Zigpy device."""
return self._zigpy_device
@property
def channels(self) -> zha_typing.ChannelsType:
"""Return ZHA channels."""
return self._channels
@channels.setter
def channels(self, value: zha_typing.ChannelsType) -> None:
"""Channels setter."""
assert isinstance(value, channels.Channels)
self._channels = value
@property
def name(self):
"""Return device name."""
@ -218,11 +235,6 @@ class ZHADevice(LogMixin):
"""Return the gateway for this device."""
return self._zha_gateway
@property
def all_channels(self):
"""Return cluster channels and relay channels for device."""
return self._all_channels
@property
def device_automation_triggers(self):
"""Return the device automation triggers for this device."""
@ -244,6 +256,19 @@ class ZHADevice(LogMixin):
"""Set availability from restore and prevent signals."""
self._available = available
@classmethod
def new(
cls,
hass: HomeAssistantType,
zigpy_dev: zha_typing.ZigpyDeviceType,
gateway: zha_typing.ZhaGatewayType,
restored: bool = False,
):
"""Create new device."""
zha_dev = cls(hass, zigpy_dev, gateway)
zha_dev.channels = channels.Channels.new(zha_dev)
return zha_dev
def _check_available(self, *_):
if self.last_seen is None:
self.update_available(False)
@ -252,16 +277,17 @@ class ZHADevice(LogMixin):
if difference > _KEEP_ALIVE_INTERVAL:
if self._checkins_missed_count < _CHECKIN_GRACE_PERIODS:
self._checkins_missed_count += 1
if (
CHANNEL_BASIC in self.cluster_channels
and self.manufacturer != "LUMI"
):
if self.manufacturer != "LUMI":
self.debug(
"Attempting to checkin with device - missed checkins: %s",
self._checkins_missed_count,
)
if not self._channels.pools:
return
pool = self._channels.pools[0]
basic_ch = pool.all_channels[f"{pool.id}:0"]
self.hass.async_create_task(
self.cluster_channels[CHANNEL_BASIC].get_attribute_value(
basic_ch.get_attribute_value(
ATTR_MANUFACTURER, from_cache=False
)
)
@ -304,66 +330,10 @@ class ZHADevice(LogMixin):
ATTR_DEVICE_TYPE: self.device_type,
}
def add_cluster_channel(self, cluster_channel):
"""Add cluster channel to device."""
# only keep 1 power configuration channel
if (
cluster_channel.name is CHANNEL_POWER_CONFIGURATION
and CHANNEL_POWER_CONFIGURATION in self.cluster_channels
):
return
if isinstance(cluster_channel, EventRelayChannel):
self._relay_channels[cluster_channel.unique_id] = cluster_channel
self._all_channels.append(cluster_channel)
else:
self.cluster_channels[cluster_channel.name] = cluster_channel
self._all_channels.append(cluster_channel)
def get_channels_to_configure(self):
"""Get a deduped list of channels for configuration.
This goes through all channels and gets a unique list of channels to
configure. It first assembles a unique list of channels that are part
of entities while stashing relay channels off to the side. It then
takse the stashed relay channels and adds them to the list of channels
that will be returned if there isn't a channel in the list for that
cluster already. This is done to ensure each cluster is only configured
once.
"""
channel_keys = []
channels = []
relay_channels = self._relay_channels.values()
def get_key(channel):
channel_key = "ZDO"
if hasattr(channel.cluster, "cluster_id"):
channel_key = "{}_{}".format(
channel.cluster.endpoint.endpoint_id, channel.cluster.cluster_id
)
return channel_key
# first we get all unique non event channels
for channel in self.all_channels:
c_key = get_key(channel)
if c_key not in channel_keys and channel not in relay_channels:
channel_keys.append(c_key)
channels.append(channel)
# now we get event channels that still need their cluster configured
for channel in relay_channels:
channel_key = get_key(channel)
if channel_key not in channel_keys:
channel_keys.append(channel_key)
channels.append(channel)
return channels
async def async_configure(self):
"""Configure the device."""
self.debug("started configuration")
await self._execute_channel_tasks(
self.get_channels_to_configure(), "async_configure"
)
await self._channels.async_configure()
self.debug("completed configuration")
entry = self.gateway.zha_storage.async_create_or_update(self)
self.debug("stored in registry: %s", entry)
@ -371,41 +341,11 @@ class ZHADevice(LogMixin):
async def async_initialize(self, from_cache=False):
"""Initialize channels."""
self.debug("started initialization")
await self._execute_channel_tasks(
self.all_channels, "async_initialize", from_cache
)
await self._channels.async_initialize(from_cache)
self.debug("power source: %s", self.power_source)
self.status = DeviceStatus.INITIALIZED
self.debug("completed initialization")
async def _execute_channel_tasks(self, channels, task_name, *args):
"""Gather and execute a set of CHANNEL tasks."""
channel_tasks = []
semaphore = asyncio.Semaphore(3)
zdo_task = None
for channel in channels:
if channel.name == CHANNEL_ZDO:
if zdo_task is None: # We only want to do this once
zdo_task = self._async_create_task(
semaphore, channel, task_name, *args
)
else:
channel_tasks.append(
self._async_create_task(semaphore, channel, task_name, *args)
)
if zdo_task is not None:
await zdo_task
await asyncio.gather(*channel_tasks)
async def _async_create_task(self, semaphore, channel, func_name, *args):
"""Configure a single channel on this device."""
try:
async with semaphore:
await getattr(channel, func_name)(*args)
channel.debug("channel: '%s' stage succeeded", func_name)
except Exception as ex: # pylint: disable=broad-except
channel.warning("channel: '%s' stage failed ex: %s", func_name, ex)
@callback
def async_unsub_dispatcher(self):
"""Unsubscribe the dispatcher."""