Skip to content

Commit

Permalink
Allow multiple attribute reads in ZHA (home-assistant#32498)
Browse files Browse the repository at this point in the history
* multi attribute reads for lights

* catch specific exceptions

* get attributes

* fix mains powered update

* add guards and use get_attributes

* use debug for read failures

* cleanup

* update return value for read_attributes

* fix on with timed off
  • Loading branch information
dmulcahey authored Mar 7, 2020
1 parent dd91b51 commit e52542c
Show file tree
Hide file tree
Showing 14 changed files with 136 additions and 78 deletions.
4 changes: 3 additions & 1 deletion homeassistant/components/zha/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ async def async_update(self):
"""Attempt to retrieve on off state from the binary sensor."""
await super().async_update()
attribute = getattr(self._channel, "value_attribute", "on_off")
self._state = await self._channel.get_attribute_value(attribute)
attr_value = await self._channel.get_attribute_value(attribute)
if attr_value is not None:
self._state = attr_value


@STRICT_MATCH(channel_names=CHANNEL_ACCELEROMETER)
Expand Down
10 changes: 10 additions & 0 deletions homeassistant/components/zha/core/channels/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ def nwk(self) -> int:
"""Device NWK for logging."""
return self._channels.zha_device.nwk

@property
def is_mains_powered(self) -> bool:
"""Device is_mains_powered."""
return self._channels.zha_device.is_mains_powered

@property
def manufacturer(self) -> Optional[str]:
"""Return device manufacturer."""
Expand All @@ -201,6 +206,11 @@ def manufacturer_code(self) -> Optional[int]:
"""Return device manufacturer."""
return self._channels.zha_device.manufacturer_code

@property
def hass(self):
"""Return hass."""
return self._channels.zha_device.hass

@property
def model(self) -> Optional[str]:
"""Return device model."""
Expand Down
33 changes: 30 additions & 3 deletions homeassistant/components/zha/core/channels/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
REPORT_CONFIG_RPT_CHANGE,
SIGNAL_ATTR_UPDATED,
)
from ..helpers import LogMixin, get_attr_id_by_name, safe_read
from ..helpers import LogMixin, safe_read

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -98,7 +98,7 @@ def __init__(
if not hasattr(self, "_value_attribute") and len(self._report_config) > 0:
attr = self._report_config[0].get("attr")
if isinstance(attr, str):
self.value_attribute = get_attr_id_by_name(self.cluster, attr)
self.value_attribute = self.cluster.attridx.get(attr)
else:
self.value_attribute = attr
self._status = ChannelStatus.CREATED
Expand Down Expand Up @@ -212,8 +212,11 @@ async def async_configure(self):
async def async_initialize(self, from_cache):
"""Initialize channel."""
self.debug("initializing channel: from_cache: %s", from_cache)
attributes = []
for report_config in self._report_config:
await self.get_attribute_value(report_config["attr"], from_cache=from_cache)
attributes.append(report_config["attr"])
if len(attributes) > 0:
await self.get_attributes(attributes, from_cache=from_cache)
self._status = ChannelStatus.INITIALIZED

@callback
Expand Down Expand Up @@ -267,6 +270,30 @@ async def get_attribute_value(self, attribute, from_cache=True):
)
return result.get(attribute)

async def get_attributes(self, attributes, from_cache=True):
"""Get the values for a list of attributes."""
manufacturer = None
manufacturer_code = self._ch_pool.manufacturer_code
if self.cluster.cluster_id >= 0xFC00 and manufacturer_code:
manufacturer = manufacturer_code
try:
result, _ = await self.cluster.read_attributes(
attributes,
allow_cache=from_cache,
only_cache=from_cache,
manufacturer=manufacturer,
)
results = {attribute: result.get(attribute) for attribute in attributes}
except (asyncio.TimeoutError, zigpy.exceptions.DeliveryError) as ex:
self.debug(
"failed to get attributes '%s' on '%s' cluster: %s",
attributes,
self.cluster.ep_attribute,
str(ex),
)
results = {}
return results

def log(self, level, msg, *args):
"""Log a message."""
msg = f"[%s:%s]: {msg}"
Expand Down
21 changes: 11 additions & 10 deletions homeassistant/components/zha/core/channels/closures.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ class DoorLockChannel(ZigbeeChannel):
async def async_update(self):
"""Retrieve latest state."""
result = await self.get_attribute_value("lock_state", from_cache=True)

self.async_send_signal(
f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", 0, "lock_state", result
)
if result is not None:
self.async_send_signal(
f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", 0, "lock_state", result
)

@callback
def attribute_updated(self, attrid, value):
Expand Down Expand Up @@ -67,12 +67,13 @@ async def async_update(self):
"current_position_lift_percentage", from_cache=False
)
self.debug("read current position: %s", result)
self.async_send_signal(
f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}",
8,
"current_position_lift_percentage",
result,
)
if result is not None:
self.async_send_signal(
f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}",
8,
"current_position_lift_percentage",
result,
)

@callback
def attribute_updated(self, attrid, value):
Expand Down
37 changes: 20 additions & 17 deletions homeassistant/components/zha/core/channels/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
SIGNAL_SET_LEVEL,
SIGNAL_STATE_ATTR,
)
from ..helpers import get_attr_id_by_name
from .base import ZigbeeChannel, parse_and_log_command

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -90,9 +89,11 @@ async def async_configure(self):

async def async_initialize(self, from_cache):
"""Initialize channel."""
self._power_source = await self.get_attribute_value(
power_source = await self.get_attribute_value(
"power_source", from_cache=from_cache
)
if power_source is not None:
self._power_source = power_source
await super().async_initialize(from_cache)

def get_power_source(self):
Expand Down Expand Up @@ -269,7 +270,7 @@ def cluster_command(self, tsn, command_id, args):
self.attribute_updated(self.ON_OFF, True)
if on_time > 0:
self._off_listener = async_call_later(
self.device.hass,
self._ch_pool.hass,
(on_time / 10), # value is in 10ths of a second
self.set_to_off,
)
Expand All @@ -293,19 +294,20 @@ def attribute_updated(self, attrid, value):

async def async_initialize(self, from_cache):
"""Initialize channel."""
self._state = bool(
await self.get_attribute_value(self.ON_OFF, from_cache=from_cache)
)
state = await self.get_attribute_value(self.ON_OFF, from_cache=from_cache)
if state is not None:
self._state = bool(state)
await super().async_initialize(from_cache)

async def async_update(self):
"""Initialize channel."""
if self.cluster.is_client:
return
self.debug("attempting to update onoff state - from cache: False")
self._state = bool(
await self.get_attribute_value(self.ON_OFF, from_cache=False)
)
from_cache = not self._ch_pool.is_mains_powered
self.debug("attempting to update onoff state - from cache: %s", from_cache)
state = await self.get_attribute_value(self.ON_OFF, from_cache=from_cache)
if state is not None:
self._state = bool(state)
await super().async_update()


Expand Down Expand Up @@ -352,7 +354,7 @@ def attribute_updated(self, attrid, value):
"""Handle attribute updates on this cluster."""
attr = self._report_config[1].get("attr")
if isinstance(attr, str):
attr_id = get_attr_id_by_name(self.cluster, attr)
attr_id = self.cluster.attridx.get(attr)
else:
attr_id = attr
if attrid == attr_id:
Expand All @@ -379,12 +381,13 @@ async def async_update(self):

async def async_read_state(self, from_cache):
"""Read data from the cluster."""
await self.get_attribute_value("battery_size", from_cache=from_cache)
await self.get_attribute_value(
"battery_percentage_remaining", from_cache=from_cache
)
await self.get_attribute_value("battery_voltage", from_cache=from_cache)
await self.get_attribute_value("battery_quantity", from_cache=from_cache)
attributes = [
"battery_size",
"battery_percentage_remaining",
"battery_voltage",
"battery_quantity",
]
await self.get_attributes(attributes, from_cache=from_cache)


@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerProfile.cluster_id)
Expand Down
14 changes: 11 additions & 3 deletions homeassistant/components/zha/core/channels/homeautomation.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,13 @@ async def async_update(self):

# This is a polling channel. Don't allow cache.
result = await self.get_attribute_value("active_power", from_cache=False)
self.async_send_signal(
f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", 0x050B, "active_power", result
)
if result is not None:
self.async_send_signal(
f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}",
0x050B,
"active_power",
result,
)

async def async_initialize(self, from_cache):
"""Initialize channel."""
Expand All @@ -92,6 +96,8 @@ async def fetch_config(self, from_cache):
divisor = await self.get_attribute_value(
"power_divisor", from_cache=from_cache
)
if divisor is None:
divisor = 1
self._divisor = divisor

mult = await self.get_attribute_value(
Expand All @@ -101,6 +107,8 @@ async def fetch_config(self, from_cache):
mult = await self.get_attribute_value(
"power_multiplier", from_cache=from_cache
)
if mult is None:
mult = 1
self._multiplier = mult

@property
Expand Down
7 changes: 4 additions & 3 deletions homeassistant/components/zha/core/channels/hvac.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ async def async_set_speed(self, value) -> None:
async def async_update(self):
"""Retrieve latest state."""
result = await self.get_attribute_value("fan_mode", from_cache=True)
self.async_send_signal(
f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", 0, "fan_mode", result
)
if result is not None:
self.async_send_signal(
f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", 0, "fan_mode", result
)

@callback
def attribute_updated(self, attrid, value):
Expand Down
9 changes: 4 additions & 5 deletions homeassistant/components/zha/core/channels/lighting.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class ColorChannel(ZigbeeChannel):
)

def __init__(
self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType,
self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType
) -> None:
"""Initialize ColorChannel."""
super().__init__(cluster, ch_pool)
Expand All @@ -52,9 +52,8 @@ async def async_configure(self):
async def async_initialize(self, from_cache):
"""Initialize channel."""
await self.fetch_color_capabilities(True)
await self.get_attribute_value("color_temperature", from_cache=from_cache)
await self.get_attribute_value("current_x", from_cache=from_cache)
await self.get_attribute_value("current_y", from_cache=from_cache)
attributes = ["color_temperature", "current_x", "current_y"]
await self.get_attributes(attributes, from_cache=from_cache)

async def fetch_color_capabilities(self, from_cache):
"""Get the color configuration."""
Expand All @@ -72,7 +71,7 @@ async def fetch_color_capabilities(self, from_cache):
"color_temperature", from_cache=from_cache
)

if result is not self.UNSUPPORTED_ATTRIBUTE:
if result is not None and result is not self.UNSUPPORTED_ATTRIBUTE:
capabilities |= self.CAPABILITIES_COLOR_TEMP
self._color_capabilities = capabilities
await super().async_initialize(from_cache)
4 changes: 2 additions & 2 deletions homeassistant/components/zha/core/channels/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,6 @@ def attribute_updated(self, attrid, value):

async def async_initialize(self, from_cache):
"""Initialize channel."""
await self.get_attribute_value("zone_status", from_cache=from_cache)
await self.get_attribute_value("zone_state", from_cache=from_cache)
attributes = ["zone_status", "zone_state"]
await self.get_attributes(attributes, from_cache=from_cache)
await super().async_initialize(from_cache)
12 changes: 0 additions & 12 deletions homeassistant/components/zha/core/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,6 @@ async def safe_read(
return {}


def get_attr_id_by_name(cluster, attr_name):
"""Get the attribute id for a cluster attribute by its name."""
return next(
(
attrid
for attrid, (attrname, datatype) in cluster.attributes.items()
if attr_name == attrname
),
None,
)


async def get_matched_clusters(source_zha_device, target_zha_device):
"""Get matched input/output cluster pairs for 2 devices."""
source_clusters = source_zha_device.async_get_std_clusters()
Expand Down
Loading

0 comments on commit e52542c

Please sign in to comment.