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 offpull/32572/head
parent
dd91b51435
commit
e52542c4d7
|
@ -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)
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])
|
||||
|
|
Loading…
Reference in New Issue