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:
parent
36db302cc8
commit
3385893b77
40 changed files with 1918 additions and 1325 deletions
|
@ -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."""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue