Allow multiple attribute reads in ZHA (#32498)

* 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
pull/32572/head
David F. Mulcahey 2020-03-07 07:33:59 -05:00 committed by GitHub
parent dd91b51435
commit e52542c4d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 136 additions and 78 deletions

View File

@ -112,7 +112,9 @@ class BinarySensor(ZhaEntity, BinarySensorDevice):
"""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)

View File

@ -191,6 +191,11 @@ class ChannelPool:
"""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."""
@ -201,6 +206,11 @@ class ChannelPool:
"""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."""

View File

@ -26,7 +26,7 @@ from ..const import (
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__)
@ -98,7 +98,7 @@ class ZigbeeChannel(LogMixin):
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
@ -212,8 +212,11 @@ class ZigbeeChannel(LogMixin):
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
@ -267,6 +270,30 @@ class ZigbeeChannel(LogMixin):
)
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}"

View File

@ -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):
@ -67,12 +67,13 @@ class WindowCovering(ZigbeeChannel):
"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):

View File

@ -17,7 +17,6 @@ from ..const import (
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__)
@ -90,9 +89,11 @@ class BasicChannel(ZigbeeChannel):
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):
@ -269,7 +270,7 @@ class OnOffChannel(ZigbeeChannel):
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,
)
@ -293,19 +294,20 @@ class OnOffChannel(ZigbeeChannel):
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()
@ -352,7 +354,7 @@ class PowerConfigurationChannel(ZigbeeChannel):
"""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:
@ -379,12 +381,13 @@ class PowerConfigurationChannel(ZigbeeChannel):
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)

View File

@ -73,9 +73,13 @@ class ElectricalMeasurementChannel(ZigbeeChannel):
# 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."""
@ -92,6 +96,8 @@ class ElectricalMeasurementChannel(ZigbeeChannel):
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(
@ -101,6 +107,8 @@ class ElectricalMeasurementChannel(ZigbeeChannel):
mult = await self.get_attribute_value(
"power_multiplier", from_cache=from_cache
)
if mult is None:
mult = 1
self._multiplier = mult
@property

View File

@ -40,9 +40,10 @@ class FanChannel(ZigbeeChannel):
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):

View File

@ -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)
@ -52,9 +52,8 @@ class ColorChannel(ZigbeeChannel):
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."""
@ -72,7 +71,7 @@ class ColorChannel(ZigbeeChannel):
"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)

View File

@ -176,6 +176,6 @@ class IASZoneChannel(ZigbeeChannel):
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)

View File

@ -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()

View File

@ -329,44 +329,59 @@ class Light(ZhaEntity, light.Light):
"""Attempt to retrieve on off state from the light."""
self.debug("polling current state")
if self._on_off_channel:
self._state = await self._on_off_channel.get_attribute_value(
state = await self._on_off_channel.get_attribute_value(
"on_off", from_cache=from_cache
)
if state is not None:
self._state = state
if self._level_channel:
self._brightness = await self._level_channel.get_attribute_value(
level = await self._level_channel.get_attribute_value(
"current_level", from_cache=from_cache
)
if level is not None:
self._brightness = level
if self._color_channel:
attributes = []
color_capabilities = self._color_channel.get_color_capabilities()
if (
color_capabilities is not None
and color_capabilities & CAPABILITIES_COLOR_TEMP
):
self._color_temp = await self._color_channel.get_attribute_value(
"color_temperature", from_cache=from_cache
)
attributes.append("color_temperature")
if (
color_capabilities is not None
and color_capabilities & CAPABILITIES_COLOR_XY
):
color_x = await self._color_channel.get_attribute_value(
"current_x", from_cache=from_cache
)
color_y = await self._color_channel.get_attribute_value(
"current_y", from_cache=from_cache
)
if color_x is not None and color_y is not None:
self._hs_color = color_util.color_xy_to_hs(
float(color_x / 65535), float(color_y / 65535)
)
attributes.append("current_x")
attributes.append("current_y")
if (
color_capabilities is not None
and color_capabilities & CAPABILITIES_COLOR_LOOP
):
color_loop_active = await self._color_channel.get_attribute_value(
"color_loop_active", from_cache=from_cache
attributes.append("color_loop_active")
results = await self._color_channel.get_attributes(
attributes, from_cache=from_cache
)
if (
"color_temperature" in results
and results["color_temperature"] is not None
):
self._color_temp = results["color_temperature"]
color_x = results.get("color_x", None)
color_y = results.get("color_y", None)
if color_x is not None and color_y is not None:
self._hs_color = color_util.color_xy_to_hs(
float(color_x / 65535), float(color_y / 65535)
)
if color_loop_active is not None and color_loop_active == 1:
if (
"color_loop_active" in results
and results["color_loop_active"] is not None
):
color_loop_active = results["color_loop_active"]
if color_loop_active == 1:
self._effect = light.EFFECT_COLORLOOP
async def refresh(self, time):

View File

@ -176,10 +176,12 @@ class Battery(Sensor):
async def async_state_attr_provider(self):
"""Return device state attrs for battery sensors."""
state_attrs = {}
battery_size = await self._channel.get_attribute_value("battery_size")
attributes = ["battery_size", "battery_quantity"]
results = await self._channel.get_attributes(attributes)
battery_size = results.get("battery_size", None)
if battery_size is not None:
state_attrs["battery_size"] = BATTERY_SIZES.get(battery_size, "Unknown")
battery_quantity = await self._channel.get_attribute_value("battery_quantity")
battery_quantity = results.get("battery_quantity", None)
if battery_quantity is not None:
state_attrs["battery_quantity"] = battery_quantity
return state_attrs

View File

@ -97,4 +97,6 @@ class Switch(ZhaEntity, SwitchDevice):
"""Attempt to retrieve on off state from the switch."""
await super().async_update()
if self._on_off_channel:
self._state = await self._on_off_channel.get_attribute_value("on_off")
state = await self._on_off_channel.get_attribute_value("on_off")
if state is not None:
self._state = state

View File

@ -52,7 +52,7 @@ def patch_cluster(cluster):
cluster.configure_reporting = CoroutineMock(return_value=[0])
cluster.deserialize = Mock()
cluster.handle_cluster_request = Mock()
cluster.read_attributes = CoroutineMock()
cluster.read_attributes = CoroutineMock(return_value=[{}, {}])
cluster.read_attributes_raw = Mock()
cluster.unbind = CoroutineMock(return_value=[0])
cluster.write_attributes = CoroutineMock(return_value=[0])