ZHA channel ZCL attributes initialization (#56476)

* Add dict of attributes to initialize

* Refactor get_attributes() method

Read 5 attributes at the time.

* Add ZCL_INIT_ATTRS attribute to base Zigbee channel

* Update tests and general clusters

* Update channels to use ZCL_INIT_ATTRS

* Update channels to use ZCL_INIT_ATTRS

* Fix tests

* Refactor async_initialize() to be a retryable request

* Maky pylint happy again
This commit is contained in:
Alexei Chetroi 2021-09-22 11:34:30 -04:00 committed by GitHub
parent 2478ec887a
commit a5d405700c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 141 additions and 179 deletions

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import asyncio import asyncio
from enum import Enum from enum import Enum
from functools import wraps from functools import partialmethod, wraps
import logging import logging
from typing import Any from typing import Any
@ -30,8 +30,9 @@ from ..const import (
ZHA_CHANNEL_MSG_BIND, ZHA_CHANNEL_MSG_BIND,
ZHA_CHANNEL_MSG_CFG_RPT, ZHA_CHANNEL_MSG_CFG_RPT,
ZHA_CHANNEL_MSG_DATA, ZHA_CHANNEL_MSG_DATA,
ZHA_CHANNEL_READS_PER_REQ,
) )
from ..helpers import LogMixin, safe_read from ..helpers import LogMixin, retryable_req, safe_read
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -92,6 +93,11 @@ class ZigbeeChannel(LogMixin):
REPORT_CONFIG: tuple[dict[int | str, tuple[int, int, int | float]]] = () REPORT_CONFIG: tuple[dict[int | str, tuple[int, int, int | float]]] = ()
BIND: bool = True BIND: bool = True
# Dict of attributes to read on channel initialization.
# Dict keys -- attribute ID or names, with bool value indicating whether a cached
# attribute read is acceptable.
ZCL_INIT_ATTRS: dict[int | str, bool] = {}
def __init__( def __init__(
self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType
) -> None: ) -> None:
@ -301,6 +307,7 @@ class ZigbeeChannel(LogMixin):
self.debug("skipping channel configuration") self.debug("skipping channel configuration")
self._status = ChannelStatus.CONFIGURED self._status = ChannelStatus.CONFIGURED
@retryable_req(delays=(1, 1, 3))
async def async_initialize(self, from_cache: bool) -> None: async def async_initialize(self, from_cache: bool) -> None:
"""Initialize channel.""" """Initialize channel."""
if not from_cache and self._ch_pool.skip_configuration: if not from_cache and self._ch_pool.skip_configuration:
@ -308,9 +315,14 @@ class ZigbeeChannel(LogMixin):
return return
self.debug("initializing channel: from_cache: %s", from_cache) self.debug("initializing channel: from_cache: %s", from_cache)
attributes = [cfg["attr"] for cfg in self.REPORT_CONFIG] cached = [a for a, cached in self.ZCL_INIT_ATTRS.items() if cached]
if attributes: uncached = [a for a, cached in self.ZCL_INIT_ATTRS.items() if not cached]
await self.get_attributes(attributes, from_cache=from_cache) uncached.extend([cfg["attr"] for cfg in self.REPORT_CONFIG])
if cached:
await self._get_attributes(True, cached, from_cache=True)
if uncached:
await self._get_attributes(True, uncached, from_cache=from_cache)
ch_specific_init = getattr(self, "async_initialize_channel_specific", None) ch_specific_init = getattr(self, "async_initialize_channel_specific", None)
if ch_specific_init: if ch_specific_init:
@ -367,28 +379,43 @@ class ZigbeeChannel(LogMixin):
) )
return result.get(attribute) return result.get(attribute)
async def get_attributes(self, attributes, from_cache=True): async def _get_attributes(
self,
raise_exceptions: bool,
attributes: list[int | str],
from_cache: bool = True,
) -> dict[int | str, Any]:
"""Get the values for a list of attributes.""" """Get the values for a list of attributes."""
manufacturer = None manufacturer = None
manufacturer_code = self._ch_pool.manufacturer_code manufacturer_code = self._ch_pool.manufacturer_code
if self.cluster.cluster_id >= 0xFC00 and manufacturer_code: if self.cluster.cluster_id >= 0xFC00 and manufacturer_code:
manufacturer = manufacturer_code manufacturer = manufacturer_code
try: chunk = attributes[:ZHA_CHANNEL_READS_PER_REQ]
result, _ = await self.cluster.read_attributes( rest = attributes[ZHA_CHANNEL_READS_PER_REQ:]
attributes, result = {}
allow_cache=from_cache, while chunk:
only_cache=from_cache and not self._ch_pool.is_mains_powered, try:
manufacturer=manufacturer, read, _ = await self.cluster.read_attributes(
) attributes,
return result allow_cache=from_cache,
except (asyncio.TimeoutError, zigpy.exceptions.ZigbeeException) as ex: only_cache=from_cache and not self._ch_pool.is_mains_powered,
self.debug( manufacturer=manufacturer,
"failed to get attributes '%s' on '%s' cluster: %s", )
attributes, result.update(read)
self.cluster.ep_attribute, except (asyncio.TimeoutError, zigpy.exceptions.ZigbeeException) as ex:
str(ex), self.debug(
) "failed to get attributes '%s' on '%s' cluster: %s",
return {} attributes,
self.cluster.ep_attribute,
str(ex),
)
if raise_exceptions:
raise
chunk = rest[:ZHA_CHANNEL_READS_PER_REQ]
rest = rest[ZHA_CHANNEL_READS_PER_REQ:]
return result
get_attributes = partialmethod(_get_attributes, False)
def log(self, level, msg, *args): def log(self, level, msg, *args):
"""Log a message.""" """Log a message."""

View File

@ -23,7 +23,6 @@ from ..const import (
SIGNAL_SET_LEVEL, SIGNAL_SET_LEVEL,
SIGNAL_UPDATE_DEVICE, SIGNAL_UPDATE_DEVICE,
) )
from ..helpers import retryable_req
from .base import ClientChannel, ZigbeeChannel, parse_and_log_command from .base import ClientChannel, ZigbeeChannel, parse_and_log_command
@ -44,7 +43,16 @@ class AnalogInput(ZigbeeChannel):
class AnalogOutput(ZigbeeChannel): class AnalogOutput(ZigbeeChannel):
"""Analog Output channel.""" """Analog Output channel."""
REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] REPORT_CONFIG = ({"attr": "present_value", "config": REPORT_CONFIG_DEFAULT},)
ZCL_INIT_ATTRS = {
"min_present_value": True,
"max_present_value": True,
"resolution": True,
"relinquish_default": True,
"description": True,
"engineering_units": True,
"application_type": True,
}
@property @property
def present_value(self) -> float | None: def present_value(self) -> float | None:
@ -99,25 +107,6 @@ class AnalogOutput(ZigbeeChannel):
return True return True
return False return False
@retryable_req(delays=(1, 1, 3))
def async_initialize_channel_specific(self, from_cache: bool) -> Coroutine:
"""Initialize channel."""
return self.fetch_config(from_cache)
async def fetch_config(self, from_cache: bool) -> None:
"""Get the channel configuration."""
attributes = [
"min_present_value",
"max_present_value",
"resolution",
"relinquish_default",
"description",
"engineering_units",
"application_type",
]
# just populates the cache, if not already done
await self.get_attributes(attributes, from_cache=from_cache)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogValue.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogValue.cluster_id)
class AnalogValue(ZigbeeChannel): class AnalogValue(ZigbeeChannel):

View File

@ -1,8 +1,6 @@
"""Home automation channels module for Zigbee Home Automation.""" """Home automation channels module for Zigbee Home Automation."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Coroutine
from zigpy.zcl.clusters import homeautomation from zigpy.zcl.clusters import homeautomation
from .. import registries from .. import registries
@ -49,6 +47,12 @@ class ElectricalMeasurementChannel(ZigbeeChannel):
CHANNEL_NAME = CHANNEL_ELECTRICAL_MEASUREMENT CHANNEL_NAME = CHANNEL_ELECTRICAL_MEASUREMENT
REPORT_CONFIG = ({"attr": "active_power", "config": REPORT_CONFIG_DEFAULT},) REPORT_CONFIG = ({"attr": "active_power", "config": REPORT_CONFIG_DEFAULT},)
ZCL_INIT_ATTRS = {
"ac_power_divisor": True,
"power_divisor": True,
"ac_power_multiplier": True,
"power_multiplier": True,
}
async def async_update(self): async def async_update(self):
"""Retrieve latest state.""" """Retrieve latest state."""
@ -64,19 +68,6 @@ class ElectricalMeasurementChannel(ZigbeeChannel):
result, result,
) )
def async_initialize_channel_specific(self, from_cache: bool) -> Coroutine:
"""Initialize channel specific attributes."""
return self.get_attributes(
[
"ac_power_divisor",
"power_divisor",
"ac_power_multiplier",
"power_multiplier",
],
from_cache=True,
)
@property @property
def divisor(self) -> int | None: def divisor(self) -> int | None:
"""Return active power divisor.""" """Return active power divisor."""

View File

@ -15,14 +15,13 @@ from zigpy.zcl.foundation import Status
from homeassistant.core import callback from homeassistant.core import callback
from .. import registries, typing as zha_typing from .. import registries
from ..const import ( from ..const import (
REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MAX_INT,
REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MIN_INT,
REPORT_CONFIG_OP, REPORT_CONFIG_OP,
SIGNAL_ATTR_UPDATED, SIGNAL_ATTR_UPDATED,
) )
from ..helpers import retryable_req
from .base import ZigbeeChannel from .base import ZigbeeChannel
AttributeUpdateRecord = namedtuple("AttributeUpdateRecord", "attr_id, attr_name, value") AttributeUpdateRecord = namedtuple("AttributeUpdateRecord", "attr_id, attr_name, value")
@ -43,12 +42,18 @@ class FanChannel(ZigbeeChannel):
_value_attribute = 0 _value_attribute = 0
REPORT_CONFIG = ({"attr": "fan_mode", "config": REPORT_CONFIG_OP},) REPORT_CONFIG = ({"attr": "fan_mode", "config": REPORT_CONFIG_OP},)
ZCL_INIT_ATTRS = {"fan_mode_sequence": True}
@property @property
def fan_mode(self) -> int | None: def fan_mode(self) -> int | None:
"""Return current fan mode.""" """Return current fan mode."""
return self.cluster.get("fan_mode") return self.cluster.get("fan_mode")
@property
def fan_mode_sequence(self) -> int | None:
"""Return possible fan mode speeds."""
return self.cluster.get("fan_mode_sequence")
async def async_set_speed(self, value) -> None: async def async_set_speed(self, value) -> None:
"""Set the speed of the fan.""" """Set the speed of the fan."""
@ -97,34 +102,17 @@ class ThermostatChannel(ZigbeeChannel):
{"attr": "pi_cooling_demand", "config": REPORT_CONFIG_CLIMATE_DEMAND}, {"attr": "pi_cooling_demand", "config": REPORT_CONFIG_CLIMATE_DEMAND},
{"attr": "pi_heating_demand", "config": REPORT_CONFIG_CLIMATE_DEMAND}, {"attr": "pi_heating_demand", "config": REPORT_CONFIG_CLIMATE_DEMAND},
) )
ZCL_INIT_ATTRS: dict[int | str, bool] = {
def __init__( "abs_min_heat_setpoint_limit": True,
self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType "abs_max_heat_setpoint_limit": True,
) -> None: "abs_min_cool_setpoint_limit": True,
"""Init Thermostat channel instance.""" "abs_max_cool_setpoint_limit": True,
super().__init__(cluster, ch_pool) "ctrl_seqe_of_oper": False,
self._init_attrs = { "max_cool_setpoint_limit": True,
"abs_min_heat_setpoint_limit": True, "max_heat_setpoint_limit": True,
"abs_max_heat_setpoint_limit": True, "min_cool_setpoint_limit": True,
"abs_min_cool_setpoint_limit": True, "min_heat_setpoint_limit": True,
"abs_max_cool_setpoint_limit": True, }
"ctrl_seqe_of_oper": False,
"local_temp": False,
"max_cool_setpoint_limit": True,
"max_heat_setpoint_limit": True,
"min_cool_setpoint_limit": True,
"min_heat_setpoint_limit": True,
"occupancy": False,
"occupied_cooling_setpoint": False,
"occupied_heating_setpoint": False,
"pi_cooling_demand": False,
"pi_heating_demand": False,
"running_mode": False,
"running_state": False,
"system_mode": False,
"unoccupied_heating_setpoint": False,
"unoccupied_cooling_setpoint": False,
}
@property @property
def abs_max_cool_setpoint_limit(self) -> int: def abs_max_cool_setpoint_limit(self) -> int:
@ -250,32 +238,6 @@ class ThermostatChannel(ZigbeeChannel):
AttributeUpdateRecord(attrid, attr_name, value), AttributeUpdateRecord(attrid, attr_name, value),
) )
async def _chunk_attr_read(self, attrs, cached=False):
chunk, attrs = attrs[:4], attrs[4:]
while chunk:
res, fail = await self.cluster.read_attributes(chunk, allow_cache=cached)
self.debug("read attributes: Success: %s. Failed: %s", res, fail)
for attr in chunk:
self._init_attrs.pop(attr, None)
if attr in fail:
continue
self.async_send_signal(
f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}",
AttributeUpdateRecord(None, attr, res[attr]),
)
chunk, attrs = attrs[:4], attrs[4:]
@retryable_req(delays=(1, 1, 3))
async def async_initialize_channel_specific(self, from_cache: bool) -> None:
"""Initialize channel."""
cached = [a for a, cached in self._init_attrs.items() if cached]
uncached = [a for a, cached in self._init_attrs.items() if not cached]
await self._chunk_attr_read(cached, cached=True)
await self._chunk_attr_read(uncached, cached=False)
async def async_set_operation_mode(self, mode) -> bool: async def async_set_operation_mode(self, mode) -> bool:
"""Set Operation mode.""" """Set Operation mode."""
if not await self.write_attributes({"system_mode": mode}): if not await self.write_attributes({"system_mode": mode}):

View File

@ -1,7 +1,6 @@
"""Lighting channels module for Zigbee Home Automation.""" """Lighting channels module for Zigbee Home Automation."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Coroutine
from contextlib import suppress from contextlib import suppress
from zigpy.zcl.clusters import lighting from zigpy.zcl.clusters import lighting
@ -36,6 +35,12 @@ class ColorChannel(ZigbeeChannel):
) )
MAX_MIREDS: int = 500 MAX_MIREDS: int = 500
MIN_MIREDS: int = 153 MIN_MIREDS: int = 153
ZCL_INIT_ATTRS = {
"color_temp_physical_min": True,
"color_temp_physical_max": True,
"color_capabilities": True,
"color_loop_active": False,
}
@property @property
def color_capabilities(self) -> int: def color_capabilities(self) -> int:
@ -75,22 +80,3 @@ class ColorChannel(ZigbeeChannel):
def max_mireds(self) -> int: def max_mireds(self) -> int:
"""Return the warmest color_temp that this channel supports.""" """Return the warmest color_temp that this channel supports."""
return self.cluster.get("color_temp_physical_max", self.MAX_MIREDS) return self.cluster.get("color_temp_physical_max", self.MAX_MIREDS)
def async_configure_channel_specific(self) -> Coroutine:
"""Configure channel."""
return self.fetch_color_capabilities(False)
def async_initialize_channel_specific(self, from_cache: bool) -> Coroutine:
"""Initialize channel."""
return self.fetch_color_capabilities(True)
async def fetch_color_capabilities(self, from_cache: bool) -> None:
"""Get the color configuration."""
attributes = [
"color_temp_physical_min",
"color_temp_physical_max",
"color_capabilities",
"color_temperature",
]
# just populates the cache, if not already done
await self.get_attributes(attributes, from_cache=from_cache)

View File

@ -7,7 +7,6 @@ https://home-assistant.io/integrations/zha/
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections.abc import Coroutine
import logging import logging
from zigpy.exceptions import ZigbeeException from zigpy.exceptions import ZigbeeException
@ -345,6 +344,8 @@ class IasWd(ZigbeeChannel):
class IASZoneChannel(ZigbeeChannel): class IASZoneChannel(ZigbeeChannel):
"""Channel for the IASZone Zigbee cluster.""" """Channel for the IASZone Zigbee cluster."""
ZCL_INIT_ATTRS = {"zone_status": True, "zone_state": False, "zone_type": True}
@callback @callback
def cluster_command(self, tsn, command_id, args): def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster.""" """Handle commands received to this cluster."""
@ -404,8 +405,3 @@ class IASZoneChannel(ZigbeeChannel):
self.cluster.attributes.get(attrid, [attrid])[0], self.cluster.attributes.get(attrid, [attrid])[0],
value, value,
) )
def async_initialize_channel_specific(self, from_cache: bool) -> Coroutine:
"""Initialize channel."""
attributes = ["zone_status", "zone_state", "zone_type"]
return self.get_attributes(attributes, from_cache=from_cache)

View File

@ -1,8 +1,6 @@
"""Smart energy channels module for Zigbee Home Automation.""" """Smart energy channels module for Zigbee Home Automation."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Coroutine
from zigpy.zcl.clusters import smartenergy from zigpy.zcl.clusters import smartenergy
from homeassistant.const import ( from homeassistant.const import (
@ -63,7 +61,13 @@ class Messaging(ZigbeeChannel):
class Metering(ZigbeeChannel): class Metering(ZigbeeChannel):
"""Metering channel.""" """Metering channel."""
REPORT_CONFIG = [{"attr": "instantaneous_demand", "config": REPORT_CONFIG_DEFAULT}] REPORT_CONFIG = ({"attr": "instantaneous_demand", "config": REPORT_CONFIG_DEFAULT},)
ZCL_INIT_ATTRS = {
"divisor": True,
"multiplier": True,
"unit_of_measure": True,
"demand_formatting": True,
}
unit_of_measure_map = { unit_of_measure_map = {
0x00: POWER_WATT, 0x00: POWER_WATT,
@ -98,14 +102,6 @@ class Metering(ZigbeeChannel):
"""Return multiplier for the value.""" """Return multiplier for the value."""
return self.cluster.get("multiplier") or 1 return self.cluster.get("multiplier") or 1
def async_configure_channel_specific(self) -> Coroutine:
"""Configure channel."""
return self.fetch_config(False)
def async_initialize_channel_specific(self, from_cache: bool) -> Coroutine:
"""Initialize channel."""
return self.fetch_config(True)
@callback @callback
def attribute_updated(self, attrid: int, value: int) -> None: def attribute_updated(self, attrid: int, value: int) -> None:
"""Handle attribute update from Metering cluster.""" """Handle attribute update from Metering cluster."""
@ -119,14 +115,10 @@ class Metering(ZigbeeChannel):
uom = self.cluster.get("unit_of_measure", 0x7F) uom = self.cluster.get("unit_of_measure", 0x7F)
return self.unit_of_measure_map.get(uom & 0x7F, "unknown") return self.unit_of_measure_map.get(uom & 0x7F, "unknown")
async def fetch_config(self, from_cache: bool) -> None: async def async_initialize_channel_specific(self, from_cache: bool) -> None:
"""Fetch config from device and updates format specifier.""" """Fetch config from device and updates format specifier."""
results = await self.get_attributes(
["divisor", "multiplier", "unit_of_measure", "demand_formatting"],
from_cache=from_cache,
)
fmting = results.get( fmting = self.cluster.get(
"demand_formatting", 0xF9 "demand_formatting", 0xF9
) # 1 digit to the right, 15 digits to the left ) # 1 digit to the right, 15 digits to the left

View File

@ -374,6 +374,7 @@ ZHA_CHANNEL_MSG_BIND = "zha_channel_bind"
ZHA_CHANNEL_MSG_CFG_RPT = "zha_channel_configure_reporting" ZHA_CHANNEL_MSG_CFG_RPT = "zha_channel_configure_reporting"
ZHA_CHANNEL_MSG_DATA = "zha_channel_msg_data" ZHA_CHANNEL_MSG_DATA = "zha_channel_msg_data"
ZHA_CHANNEL_CFG_DONE = "zha_channel_cfg_done" ZHA_CHANNEL_CFG_DONE = "zha_channel_cfg_done"
ZHA_CHANNEL_READS_PER_REQ = 5
ZHA_GW_MSG = "zha_gateway_message" ZHA_GW_MSG = "zha_gateway_message"
ZHA_GW_MSG_DEVICE_FULL_INIT = "device_fully_initialized" ZHA_GW_MSG_DEVICE_FULL_INIT = "device_fully_initialized"
ZHA_GW_MSG_DEVICE_INFO = "device_info" ZHA_GW_MSG_DEVICE_INFO = "device_info"

View File

@ -56,6 +56,18 @@ def patch_cluster(cluster):
cluster.add = AsyncMock(return_value=[0]) cluster.add = AsyncMock(return_value=[0])
def update_attribute_cache(cluster):
"""Update attribute cache based on plugged attributes."""
if cluster.PLUGGED_ATTR_READS:
attrs = [
make_attribute(cluster.attridx.get(attr, attr), value)
for attr, value in cluster.PLUGGED_ATTR_READS.items()
]
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
hdr.frame_control.disable_default_response = True
cluster.handle_message(hdr, [attrs])
def get_zha_gateway(hass): def get_zha_gateway(hass):
"""Return ZHA gateway from hass.data.""" """Return ZHA gateway from hass.data."""
try: try:

View File

@ -476,7 +476,7 @@ async def test_fan_update_entity(
assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == 0 assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == 0
assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None
assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE_STEP] == 100 / 3 assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE_STEP] == 100 / 3
assert cluster.read_attributes.await_count == 1 assert cluster.read_attributes.await_count == 2
await async_setup_component(hass, "homeassistant", {}) await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done() await hass.async_block_till_done()
@ -486,7 +486,7 @@ async def test_fan_update_entity(
) )
assert hass.states.get(entity_id).state == STATE_OFF assert hass.states.get(entity_id).state == STATE_OFF
assert hass.states.get(entity_id).attributes[ATTR_SPEED] == SPEED_OFF assert hass.states.get(entity_id).attributes[ATTR_SPEED] == SPEED_OFF
assert cluster.read_attributes.await_count == 2 assert cluster.read_attributes.await_count == 3
cluster.PLUGGED_ATTR_READS = {"fan_mode": 1} cluster.PLUGGED_ATTR_READS = {"fan_mode": 1}
await hass.services.async_call( await hass.services.async_call(
@ -497,4 +497,4 @@ async def test_fan_update_entity(
assert hass.states.get(entity_id).attributes[ATTR_SPEED] == SPEED_LOW assert hass.states.get(entity_id).attributes[ATTR_SPEED] == SPEED_LOW
assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None
assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE_STEP] == 100 / 3 assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE_STEP] == 100 / 3
assert cluster.read_attributes.await_count == 3 assert cluster.read_attributes.await_count == 4

View File

@ -16,6 +16,7 @@ from .common import (
async_test_rejoin, async_test_rejoin,
find_entity_id, find_entity_id,
send_attributes_report, send_attributes_report,
update_attribute_cache,
) )
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE
@ -41,25 +42,30 @@ async def test_number(hass, zha_device_joined_restored, zigpy_analog_output_devi
cluster = zigpy_analog_output_device.endpoints.get(1).analog_output cluster = zigpy_analog_output_device.endpoints.get(1).analog_output
cluster.PLUGGED_ATTR_READS = { cluster.PLUGGED_ATTR_READS = {
"present_value": 15.0,
"max_present_value": 100.0, "max_present_value": 100.0,
"min_present_value": 0.0, "min_present_value": 1.0,
"relinquish_default": 50.0, "relinquish_default": 50.0,
"resolution": 1.0, "resolution": 1.1,
"description": "PWM1", "description": "PWM1",
"engineering_units": 98, "engineering_units": 98,
"application_type": 4 * 0x10000, "application_type": 4 * 0x10000,
} }
update_attribute_cache(cluster)
cluster.PLUGGED_ATTR_READS["present_value"] = 15.0
zha_device = await zha_device_joined_restored(zigpy_analog_output_device) zha_device = await zha_device_joined_restored(zigpy_analog_output_device)
# one for present_value and one for the rest configuration attributes # one for present_value and one for the rest configuration attributes
assert cluster.read_attributes.call_count == 2 assert cluster.read_attributes.call_count == 3
assert "max_present_value" in cluster.read_attributes.call_args[0][0] attr_reads = set()
assert "min_present_value" in cluster.read_attributes.call_args[0][0] for call_args in cluster.read_attributes.call_args_list:
assert "relinquish_default" in cluster.read_attributes.call_args[0][0] attr_reads |= set(call_args[0][0])
assert "resolution" in cluster.read_attributes.call_args[0][0] assert "max_present_value" in attr_reads
assert "description" in cluster.read_attributes.call_args[0][0] assert "min_present_value" in attr_reads
assert "engineering_units" in cluster.read_attributes.call_args[0][0] assert "relinquish_default" in attr_reads
assert "application_type" in cluster.read_attributes.call_args[0][0] assert "resolution" in attr_reads
assert "description" in attr_reads
assert "engineering_units" in attr_reads
assert "application_type" in attr_reads
entity_id = await find_entity_id(DOMAIN, zha_device, hass) entity_id = await find_entity_id(DOMAIN, zha_device, hass)
assert entity_id is not None assert entity_id is not None
@ -69,18 +75,18 @@ async def test_number(hass, zha_device_joined_restored, zigpy_analog_output_devi
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
# allow traffic to flow through the gateway and device # allow traffic to flow through the gateway and device
assert cluster.read_attributes.call_count == 2 assert cluster.read_attributes.call_count == 3
await async_enable_traffic(hass, [zha_device]) await async_enable_traffic(hass, [zha_device])
await hass.async_block_till_done() await hass.async_block_till_done()
assert cluster.read_attributes.call_count == 4 assert cluster.read_attributes.call_count == 6
# test that the state has changed from unavailable to 15.0 # test that the state has changed from unavailable to 15.0
assert hass.states.get(entity_id).state == "15.0" assert hass.states.get(entity_id).state == "15.0"
# test attributes # test attributes
assert hass.states.get(entity_id).attributes.get("min") == 0.0 assert hass.states.get(entity_id).attributes.get("min") == 1.0
assert hass.states.get(entity_id).attributes.get("max") == 100.0 assert hass.states.get(entity_id).attributes.get("max") == 100.0
assert hass.states.get(entity_id).attributes.get("step") == 1.0 assert hass.states.get(entity_id).attributes.get("step") == 1.1
assert hass.states.get(entity_id).attributes.get("icon") == "mdi:percent" assert hass.states.get(entity_id).attributes.get("icon") == "mdi:percent"
assert hass.states.get(entity_id).attributes.get("unit_of_measurement") == "%" assert hass.states.get(entity_id).attributes.get("unit_of_measurement") == "%"
assert ( assert (
@ -89,7 +95,7 @@ async def test_number(hass, zha_device_joined_restored, zigpy_analog_output_devi
) )
# change value from device # change value from device
assert cluster.read_attributes.call_count == 4 assert cluster.read_attributes.call_count == 6
await send_attributes_report(hass, cluster, {0x0055: 15}) await send_attributes_report(hass, cluster, {0x0055: 15})
assert hass.states.get(entity_id).state == "15.0" assert hass.states.get(entity_id).state == "15.0"
@ -111,10 +117,10 @@ async def test_number(hass, zha_device_joined_restored, zigpy_analog_output_devi
cluster.PLUGGED_ATTR_READS["present_value"] = 30.0 cluster.PLUGGED_ATTR_READS["present_value"] = 30.0
# test rejoin # test rejoin
assert cluster.read_attributes.call_count == 4 assert cluster.read_attributes.call_count == 6
await async_test_rejoin(hass, zigpy_analog_output_device, [cluster], (1,)) await async_test_rejoin(hass, zigpy_analog_output_device, [cluster], (1,))
assert hass.states.get(entity_id).state == "30.0" assert hass.states.get(entity_id).state == "30.0"
assert cluster.read_attributes.call_count == 6 assert cluster.read_attributes.call_count == 9
# update device value with failed attribute report # update device value with failed attribute report
cluster.PLUGGED_ATTR_READS["present_value"] = 40.0 cluster.PLUGGED_ATTR_READS["present_value"] = 40.0
@ -128,5 +134,5 @@ async def test_number(hass, zha_device_joined_restored, zigpy_analog_output_devi
"homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True
) )
assert hass.states.get(entity_id).state == "40.0" assert hass.states.get(entity_id).state == "40.0"
assert cluster.read_attributes.call_count == 7 assert cluster.read_attributes.call_count == 10
assert "present_value" in cluster.read_attributes.call_args[0][0] assert "present_value" in cluster.read_attributes.call_args[0][0]