diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 552be260e8b..c42384682da 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -650,7 +650,7 @@ async def websocket_device_cluster_attributes( ) if attributes is not None: for attr_id, attr in attributes.items(): - cluster_attributes.append({ID: attr_id, ATTR_NAME: attr[0]}) + cluster_attributes.append({ID: attr_id, ATTR_NAME: attr.name}) _LOGGER.debug( "Requested attributes for: %s: %s, %s: '%s', %s: %s, %s: %s", ATTR_CLUSTER_ID, @@ -700,7 +700,7 @@ async def websocket_device_cluster_commands( { TYPE: CLIENT, ID: cmd_id, - ATTR_NAME: cmd[0], + ATTR_NAME: cmd.name, } ) for cmd_id, cmd in commands[CLUSTER_COMMANDS_SERVER].items(): @@ -708,7 +708,7 @@ async def websocket_device_cluster_commands( { TYPE: CLUSTER_COMMAND_SERVER, ID: cmd_id, - ATTR_NAME: cmd[0], + ATTR_NAME: cmd.name, } ) _LOGGER.debug( diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 8544c46e92e..06b1e8a47d8 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -161,9 +161,9 @@ class Thermostat(ZhaEntity, ClimateEntity): @property def current_temperature(self): """Return the current temperature.""" - if self._thrm.local_temp is None: + if self._thrm.local_temperature is None: return None - return self._thrm.local_temp / ZCL_TEMP + return self._thrm.local_temperature / ZCL_TEMP @property def extra_state_attributes(self): @@ -272,7 +272,7 @@ class Thermostat(ZhaEntity, ClimateEntity): @property def hvac_modes(self) -> tuple[str, ...]: """Return the list of available HVAC operation modes.""" - return SEQ_OF_OPERATION.get(self._thrm.ctrl_seqe_of_oper, (HVAC_MODE_OFF,)) + return SEQ_OF_OPERATION.get(self._thrm.ctrl_sequence_of_oper, (HVAC_MODE_OFF,)) @property def precision(self): diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index b63c20e14eb..2011f92a63b 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -346,7 +346,9 @@ class ChannelPool: results = await asyncio.gather(*tasks, return_exceptions=True) for channel, outcome in zip(channels, results): if isinstance(outcome, Exception): - channel.warning("'%s' stage failed: %s", func_name, str(outcome)) + channel.warning( + "'%s' stage failed: %s", func_name, str(outcome), exc_info=outcome + ) continue channel.debug("'%s' stage succeeded", func_name) diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index f79000d0646..0dd6169373b 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -8,7 +8,7 @@ import logging from typing import Any import zigpy.exceptions -from zigpy.zcl.foundation import Status +from zigpy.zcl.foundation import ConfigureReportingResponseRecord, Status from homeassistant.const import ATTR_COMMAND from homeassistant.core import callback @@ -111,7 +111,7 @@ class ZigbeeChannel(LogMixin): if not hasattr(self, "_value_attribute") and self.REPORT_CONFIG: attr = self.REPORT_CONFIG[0].get("attr") if isinstance(attr, str): - self.value_attribute = self.cluster.attridx.get(attr) + self.value_attribute = self.cluster.attributes_by_name.get(attr) else: self.value_attribute = attr self._status = ChannelStatus.CREATED @@ -260,7 +260,7 @@ class ZigbeeChannel(LogMixin): self, attrs: dict[int | str, tuple], res: list | tuple ) -> None: """Parse configure reporting result.""" - if not isinstance(res, list): + if isinstance(res, (Exception, ConfigureReportingResponseRecord)): # assume default response self.debug( "attr reporting for '%s' on '%s': %s", @@ -345,7 +345,7 @@ class ZigbeeChannel(LogMixin): self.async_send_signal( f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", attrid, - self.cluster.attributes.get(attrid, [attrid])[0], + self._get_attribute_name(attrid), value, ) @@ -368,6 +368,12 @@ class ZigbeeChannel(LogMixin): async def async_update(self): """Retrieve latest state from cluster.""" + def _get_attribute_name(self, attrid: int) -> str | int: + if attrid not in self.cluster.attributes: + return attrid + + return self.cluster.attributes[attrid].name + async def get_attribute_value(self, attribute, from_cache=True): """Get the value for an attribute.""" manufacturer = None @@ -421,11 +427,11 @@ class ZigbeeChannel(LogMixin): get_attributes = partialmethod(_get_attributes, False) - def log(self, level, msg, *args): + def log(self, level, msg, *args, **kwargs): """Log a message.""" msg = f"[%s:%s]: {msg}" args = (self._ch_pool.nwk, self._id) + args - _LOGGER.log(level, msg, *args) + _LOGGER.log(level, msg, *args, **kwargs) def __getattr__(self, name): """Get attribute or a decorated cluster command.""" @@ -479,11 +485,11 @@ class ZDOChannel(LogMixin): """Configure channel.""" self._status = ChannelStatus.CONFIGURED - def log(self, level, msg, *args): + def log(self, level, msg, *args, **kwargs): """Log a message.""" msg = f"[%s:ZDO](%s): {msg}" args = (self._zha_device.nwk, self._zha_device.model) + args - _LOGGER.log(level, msg, *args) + _LOGGER.log(level, msg, *args, **kwargs) class ClientChannel(ZigbeeChannel): @@ -492,13 +498,17 @@ class ClientChannel(ZigbeeChannel): @callback def attribute_updated(self, attrid, value): """Handle an attribute updated on this cluster.""" + + try: + attr_name = self._cluster.attributes[attrid].name + except KeyError: + attr_name = "Unknown" + self.zha_send_event( SIGNAL_ATTR_UPDATED, { ATTR_ATTRIBUTE_ID: attrid, - ATTR_ATTRIBUTE_NAME: self._cluster.attributes.get(attrid, ["Unknown"])[ - 0 - ], + ATTR_ATTRIBUTE_NAME: attr_name, ATTR_VALUE: value, }, ) @@ -510,4 +520,4 @@ class ClientChannel(ZigbeeChannel): self._cluster.server_commands is not None and self._cluster.server_commands.get(command_id) is not None ): - self.zha_send_event(self._cluster.server_commands.get(command_id)[0], args) + self.zha_send_event(self._cluster.server_commands[command_id].name, args) diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index c63d069767d..bf50c8fc4ba 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -33,7 +33,8 @@ class DoorLockChannel(ZigbeeChannel): ): return - command_name = self._cluster.client_commands.get(command_id, [command_id])[0] + command_name = self._cluster.client_commands[command_id].name + if command_name == "operation_event_notification": self.zha_send_event( command_name, @@ -47,7 +48,7 @@ class DoorLockChannel(ZigbeeChannel): @callback def attribute_updated(self, attrid, value): """Handle attribute update from lock cluster.""" - attr_name = self.cluster.attributes.get(attrid, [attrid])[0] + attr_name = self._get_attribute_name(attrid) self.debug( "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value ) @@ -140,7 +141,7 @@ class WindowCovering(ZigbeeChannel): @callback def attribute_updated(self, attrid, value): """Handle attribute update from window_covering cluster.""" - attr_name = self.cluster.attributes.get(attrid, [attrid])[0] + attr_name = self._get_attribute_name(attrid) self.debug( "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value ) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 89d750465b8..09a1fd80f17 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -103,7 +103,7 @@ class AnalogOutput(ZigbeeChannel): except zigpy.exceptions.ZigbeeException as ex: self.error("Could not set value: %s", ex) return False - if isinstance(res, list) and all( + if not isinstance(res, Exception) and all( record.status == Status.SUCCESS for record in res[0] ): return True @@ -380,7 +380,11 @@ class Ota(ZigbeeChannel): self, tsn: int, command_id: int, args: list[Any] | None ) -> None: """Handle OTA commands.""" - cmd_name = self.cluster.server_commands.get(command_id, [command_id])[0] + if command_id in self.cluster.server_commands: + cmd_name = self.cluster.server_commands[command_id].name + else: + cmd_name = command_id + signal_id = self._ch_pool.unique_id.split("-")[0] if cmd_name == "query_next_image": self.async_send_signal(SIGNAL_UPDATE_DEVICE.format(signal_id), args[3]) @@ -418,7 +422,11 @@ class PollControl(ZigbeeChannel): self, tsn: int, command_id: int, args: list[Any] | None ) -> None: """Handle commands received to this cluster.""" - cmd_name = self.cluster.client_commands.get(command_id, [command_id])[0] + if command_id in self.cluster.client_commands: + cmd_name = self.cluster.client_commands[command_id].name + else: + cmd_name = command_id + self.debug("Received %s tsn command '%s': %s", tsn, cmd_name, args) self.zha_send_event(cmd_name, args) if cmd_name == "checkin": diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 726d9f15376..5b102d062cb 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -70,7 +70,7 @@ class FanChannel(ZigbeeChannel): @callback def attribute_updated(self, attrid: int, value: Any) -> None: """Handle attribute update from fan cluster.""" - attr_name = self.cluster.attributes.get(attrid, [attrid])[0] + attr_name = self._get_attribute_name(attrid) self.debug( "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value ) @@ -90,7 +90,7 @@ class ThermostatChannel(ZigbeeChannel): """Thermostat channel.""" REPORT_CONFIG = ( - {"attr": "local_temp", "config": REPORT_CONFIG_CLIMATE}, + {"attr": "local_temperature", "config": REPORT_CONFIG_CLIMATE}, {"attr": "occupied_cooling_setpoint", "config": REPORT_CONFIG_CLIMATE}, {"attr": "occupied_heating_setpoint", "config": REPORT_CONFIG_CLIMATE}, {"attr": "unoccupied_cooling_setpoint", "config": REPORT_CONFIG_CLIMATE}, @@ -107,7 +107,7 @@ class ThermostatChannel(ZigbeeChannel): "abs_max_heat_setpoint_limit": True, "abs_min_cool_setpoint_limit": True, "abs_max_cool_setpoint_limit": True, - "ctrl_seqe_of_oper": False, + "ctrl_sequence_of_oper": False, "max_cool_setpoint_limit": True, "max_heat_setpoint_limit": True, "min_cool_setpoint_limit": True, @@ -135,9 +135,9 @@ class ThermostatChannel(ZigbeeChannel): return self.cluster.get("abs_min_heat_setpoint_limit", 700) @property - def ctrl_seqe_of_oper(self) -> int: + def ctrl_sequence_of_oper(self) -> int: """Control Sequence of operations attribute.""" - return self.cluster.get("ctrl_seqe_of_oper", 0xFF) + return self.cluster.get("ctrl_sequence_of_oper", 0xFF) @property def max_cool_setpoint_limit(self) -> int: @@ -172,9 +172,9 @@ class ThermostatChannel(ZigbeeChannel): return sp_limit @property - def local_temp(self) -> int | None: + def local_temperature(self) -> int | None: """Thermostat temperature.""" - return self.cluster.get("local_temp") + return self.cluster.get("local_temperature") @property def occupancy(self) -> int | None: @@ -229,7 +229,7 @@ class ThermostatChannel(ZigbeeChannel): @callback def attribute_updated(self, attrid, value): """Handle attribute update cluster.""" - attr_name = self.cluster.attributes.get(attrid, [attrid])[0] + attr_name = self._get_attribute_name(attrid) self.debug( "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value ) @@ -300,7 +300,7 @@ class ThermostatChannel(ZigbeeChannel): @staticmethod def check_result(res: list) -> bool: """Normalize the result.""" - if not isinstance(res, list): + if isinstance(res, Exception): return False return all(record.status == Status.SUCCESS for record in res[0]) diff --git a/homeassistant/components/zha/core/channels/lightlink.py b/homeassistant/components/zha/core/channels/lightlink.py index 46c40fdaff0..a29d9020a75 100644 --- a/homeassistant/components/zha/core/channels/lightlink.py +++ b/homeassistant/components/zha/core/channels/lightlink.py @@ -3,6 +3,7 @@ import asyncio import zigpy.exceptions from zigpy.zcl.clusters import lightlink +from zigpy.zcl.foundation import GENERAL_COMMANDS, GeneralCommand from .. import registries from .base import ChannelStatus, ZigbeeChannel @@ -30,11 +31,16 @@ class LightLink(ZigbeeChannel): return try: - _, _, groups = await self.cluster.get_group_identifiers(0) + rsp = await self.cluster.get_group_identifiers(0) except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as exc: self.warning("Couldn't get list of groups: %s", str(exc)) return + if isinstance(rsp, GENERAL_COMMANDS[GeneralCommand.Default_Response].schema): + groups = [] + else: + groups = rsp.group_info_records + if groups: for group in groups: self.debug("Adding coordinator to 0x%04x group id", group.group_id) diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 8aa5c620656..19be861178f 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -85,7 +85,7 @@ class IasAce(ZigbeeChannel): def cluster_command(self, tsn, command_id, args) -> None: """Handle commands received to this cluster.""" self.warning( - "received command %s", self._cluster.server_commands.get(command_id)[NAME] + "received command %s", self._cluster.server_commands[command_id].name ) self.command_map[command_id](*args) @@ -94,7 +94,7 @@ class IasAce(ZigbeeChannel): mode = AceCluster.ArmMode(arm_mode) self.zha_send_event( - self._cluster.server_commands.get(IAS_ACE_ARM)[NAME], + self._cluster.server_commands[IAS_ACE_ARM].name, { "arm_mode": mode.value, "arm_mode_description": mode.name, @@ -190,7 +190,7 @@ class IasAce(ZigbeeChannel): def _bypass(self, zone_list, code) -> None: """Handle the IAS ACE bypass command.""" self.zha_send_event( - self._cluster.server_commands.get(IAS_ACE_BYPASS)[NAME], + self._cluster.server_commands[IAS_ACE_BYPASS].name, {"zone_list": zone_list, "code": code}, ) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index 5877dad14fa..b153372a322 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -65,7 +65,7 @@ class Metering(ZigbeeChannel): "divisor": True, "metering_device_type": True, "multiplier": True, - "summa_formatting": True, + "summation_formatting": True, "unit_of_measure": True, } @@ -159,7 +159,7 @@ class Metering(ZigbeeChannel): self._format_spec = self.get_formatting(fmting) fmting = self.cluster.get( - "summa_formatting", 0xF9 + "summation_formatting", 0xF9 ) # 1 digit to the right, 15 digits to the left self._summa_format = self.get_formatting(fmting) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 79cc54c4829..e80a0725cc1 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -783,8 +783,8 @@ class ZHADevice(LogMixin): fmt = f"{log_msg[1]} completed: %s" zdo.debug(fmt, *(log_msg[2] + (outcome,))) - def log(self, level: int, msg: str, *args: Any) -> None: + def log(self, level: int, msg: str, *args: Any, **kwargs: dict) -> None: """Log a message.""" msg = f"[%s](%s): {msg}" args = (self.nwk, self.model) + args - _LOGGER.log(level, msg, *args) + _LOGGER.log(level, msg, *args, **kwargs) diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py index 93e96c7565b..af17f28e622 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -108,11 +108,11 @@ class ZHAGroupMember(LogMixin): str(ex), ) - def log(self, level: int, msg: str, *args: Any) -> None: + def log(self, level: int, msg: str, *args: Any, **kwargs) -> None: """Log a message.""" msg = f"[%s](%s): {msg}" args = (f"0x{self._zha_group.group_id:04x}", self.endpoint_id) + args - _LOGGER.log(level, msg, *args) + _LOGGER.log(level, msg, *args, **kwargs) class ZHAGroup(LogMixin): @@ -224,8 +224,8 @@ class ZHAGroup(LogMixin): group_info["members"] = [member.member_info for member in self.members] return group_info - def log(self, level: int, msg: str, *args: Any) -> None: + def log(self, level: int, msg: str, *args: Any, **kwargs) -> None: """Log a message.""" msg = f"[%s](%s): {msg}" args = (self.name, self.group_id) + args - _LOGGER.log(level, msg, *args) + _LOGGER.log(level, msg, *args, **kwargs) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 5e98799e387..fcd29c1619f 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -210,23 +210,23 @@ def reduce_attribute( class LogMixin: """Log helper.""" - def log(self, level, msg, *args): + def log(self, level, msg, *args, **kwargs): """Log with level.""" raise NotImplementedError - def debug(self, msg, *args): + def debug(self, msg, *args, **kwargs): """Debug level log.""" return self.log(logging.DEBUG, msg, *args) - def info(self, msg, *args): + def info(self, msg, *args, **kwargs): """Info level log.""" return self.log(logging.INFO, msg, *args) - def warning(self, msg, *args): + def warning(self, msg, *args, **kwargs): """Warning method log.""" return self.log(logging.WARNING, msg, *args) - def error(self, msg, *args): + def error(self, msg, *args, **kwargs): """Error level log.""" return self.log(logging.ERROR, msg, *args) diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 9f62d4b9c02..0fdb4daeaa5 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -133,20 +133,20 @@ class ZhaCover(ZhaEntity, CoverEntity): async def async_open_cover(self, **kwargs): """Open the window cover.""" res = await self._cover_channel.up_open() - if isinstance(res, list) and res[1] is Status.SUCCESS: + if not isinstance(res, Exception) and res[1] is Status.SUCCESS: self.async_update_state(STATE_OPENING) async def async_close_cover(self, **kwargs): """Close the window cover.""" res = await self._cover_channel.down_close() - if isinstance(res, list) and res[1] is Status.SUCCESS: + if not isinstance(res, Exception) and res[1] is Status.SUCCESS: self.async_update_state(STATE_CLOSING) async def async_set_cover_position(self, **kwargs): """Move the roller shutter to a specific position.""" new_pos = kwargs[ATTR_POSITION] res = await self._cover_channel.go_to_lift_percentage(100 - new_pos) - if isinstance(res, list) and res[1] is Status.SUCCESS: + if not isinstance(res, Exception) and res[1] is Status.SUCCESS: self.async_update_state( STATE_CLOSING if new_pos < self._current_position else STATE_OPENING ) @@ -154,7 +154,7 @@ class ZhaCover(ZhaEntity, CoverEntity): async def async_stop_cover(self, **kwargs): """Stop the window cover.""" res = await self._cover_channel.stop() - if isinstance(res, list) and res[1] is Status.SUCCESS: + if not isinstance(res, Exception) and res[1] is Status.SUCCESS: self._state = STATE_OPEN if self._current_position > 0 else STATE_CLOSED self.async_write_ha_state() @@ -250,7 +250,7 @@ class Shade(ZhaEntity, CoverEntity): async def async_open_cover(self, **kwargs): """Open the window cover.""" res = await self._on_off_channel.on() - if not isinstance(res, list) or res[1] != Status.SUCCESS: + if isinstance(res, Exception) or res[1] != Status.SUCCESS: self.debug("couldn't open cover: %s", res) return @@ -260,7 +260,7 @@ class Shade(ZhaEntity, CoverEntity): async def async_close_cover(self, **kwargs): """Close the window cover.""" res = await self._on_off_channel.off() - if not isinstance(res, list) or res[1] != Status.SUCCESS: + if isinstance(res, Exception) or res[1] != Status.SUCCESS: self.debug("couldn't open cover: %s", res) return @@ -274,7 +274,7 @@ class Shade(ZhaEntity, CoverEntity): new_pos * 255 / 100, 1 ) - if not isinstance(res, list) or res[1] != Status.SUCCESS: + if isinstance(res, Exception) or res[1] != Status.SUCCESS: self.debug("couldn't set cover's position: %s", res) return @@ -284,7 +284,7 @@ class Shade(ZhaEntity, CoverEntity): async def async_stop_cover(self, **kwargs) -> None: """Stop the cover.""" res = await self._level_channel.stop() - if not isinstance(res, list) or res[1] != Status.SUCCESS: + if isinstance(res, Exception) or res[1] != Status.SUCCESS: self.debug("couldn't stop cover: %s", res) return diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 4a9b0f7577c..13e43aa9ff0 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -139,11 +139,11 @@ class BaseZhaEntity(LogMixin, entity.Entity): ) self._unsubs.append(unsub) - def log(self, level: int, msg: str, *args): + def log(self, level: int, msg: str, *args, **kwargs): """Log a message.""" msg = f"%s: {msg}" args = (self.entity_id,) + args - _LOGGER.log(level, msg, *args) + _LOGGER.log(level, msg, *args, **kwargs) class ZhaEntity(BaseZhaEntity, RestoreEntity): diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 6855db22572..b6d344a57e7 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -243,7 +243,7 @@ class BaseLight(LogMixin, light.LightEntity): level, duration ) t_log["move_to_level_with_on_off"] = result - if not isinstance(result, list) or result[1] is not Status.SUCCESS: + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._state = bool(level) @@ -255,7 +255,7 @@ class BaseLight(LogMixin, light.LightEntity): # we should call the on command on the on_off cluster if brightness is not 0. result = await self._on_off_channel.on() t_log["on_off"] = result - if not isinstance(result, list) or result[1] is not Status.SUCCESS: + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._state = True @@ -266,7 +266,7 @@ class BaseLight(LogMixin, light.LightEntity): temperature = kwargs[light.ATTR_COLOR_TEMP] result = await self._color_channel.move_to_color_temp(temperature, duration) t_log["move_to_color_temp"] = result - if not isinstance(result, list) or result[1] is not Status.SUCCESS: + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._color_temp = temperature @@ -282,7 +282,7 @@ class BaseLight(LogMixin, light.LightEntity): int(xy_color[0] * 65535), int(xy_color[1] * 65535), duration ) t_log["move_to_color"] = result - if not isinstance(result, list) or result[1] is not Status.SUCCESS: + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._hs_color = hs_color @@ -340,7 +340,7 @@ class BaseLight(LogMixin, light.LightEntity): else: result = await self._on_off_channel.off() self.debug("turned off: %s", result) - if not isinstance(result, list) or result[1] is not Status.SUCCESS: + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: return self._state = False diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index 341cfcebf68..1ebb10cacb6 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -122,7 +122,7 @@ class ZhaDoorLock(ZhaEntity, LockEntity): async def async_lock(self, **kwargs): """Lock the lock.""" result = await self._doorlock_channel.lock_door() - if not isinstance(result, list) or result[0] is not Status.SUCCESS: + if isinstance(result, Exception) or result[0] is not Status.SUCCESS: self.error("Error with lock_door: %s", result) return self.async_write_ha_state() @@ -130,7 +130,7 @@ class ZhaDoorLock(ZhaEntity, LockEntity): async def async_unlock(self, **kwargs): """Unlock the lock.""" result = await self._doorlock_channel.unlock_door() - if not isinstance(result, list) or result[0] is not Status.SUCCESS: + if isinstance(result, Exception) or result[0] is not Status.SUCCESS: self.error("Error with unlock_door: %s", result) return self.async_write_ha_state() diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index e542c77516e..6d47535b765 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,9 +7,9 @@ "bellows==0.29.0", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.67", + "zha-quirks==0.0.69", "zigpy-deconz==0.14.0", - "zigpy==0.43.0", + "zigpy==0.44.1", "zigpy-xbee==0.14.0", "zigpy-zigate==0.8.0", "zigpy-znp==0.7.0" diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 29fb08b9bc0..87d2407c2dc 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -65,7 +65,7 @@ class BaseSwitch(SwitchEntity): async def async_turn_on(self, **kwargs) -> None: """Turn the entity on.""" result = await self._on_off_channel.on() - if not isinstance(result, list) or result[1] is not Status.SUCCESS: + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: return self._state = True self.async_write_ha_state() @@ -73,7 +73,7 @@ class BaseSwitch(SwitchEntity): async def async_turn_off(self, **kwargs) -> None: """Turn the entity off.""" result = await self._on_off_channel.off() - if not isinstance(result, list) or result[1] is not Status.SUCCESS: + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: return self._state = False self.async_write_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 63d5920ea52..445dc6aa49f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2469,7 +2469,7 @@ zengge==0.2 zeroconf==0.38.4 # homeassistant.components.zha -zha-quirks==0.0.67 +zha-quirks==0.0.69 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2490,7 +2490,7 @@ zigpy-zigate==0.8.0 zigpy-znp==0.7.0 # homeassistant.components.zha -zigpy==0.43.0 +zigpy==0.44.1 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f2ecacfb2e4..5d132c38fc5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1595,7 +1595,7 @@ youless-api==0.16 zeroconf==0.38.4 # homeassistant.components.zha -zha-quirks==0.0.67 +zha-quirks==0.0.69 # homeassistant.components.zha zigpy-deconz==0.14.0 @@ -1610,7 +1610,7 @@ zigpy-zigate==0.8.0 zigpy-znp==0.7.0 # homeassistant.components.zha -zigpy==0.43.0 +zigpy==0.44.1 # homeassistant.components.zwave_js zwave-js-server-python==0.35.2 diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 48772d31fb6..757587071fd 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -20,8 +20,10 @@ def patch_cluster(cluster): value = cluster.PLUGGED_ATTR_READS.get(attr_id) if value is None: # try converting attr_id to attr_name and lookup the plugs again - attr_name = cluster.attributes.get(attr_id) - value = attr_name and cluster.PLUGGED_ATTR_READS.get(attr_name[0]) + attr = cluster.attributes.get(attr_id) + + if attr is not None: + value = cluster.PLUGGED_ATTR_READS.get(attr.name) if value is not None: result.append( zcl_f.ReadAttributeRecord( @@ -58,14 +60,23 @@ def patch_cluster(cluster): 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]) + if not cluster.PLUGGED_ATTR_READS: + return + + attrs = [] + for attrid, value in cluster.PLUGGED_ATTR_READS.items(): + if isinstance(attrid, str): + attrid = cluster.attributes_by_name[attrid].id + else: + attrid = zigpy.types.uint16_t(attrid) + attrs.append(make_attribute(attrid, value)) + + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) + hdr.frame_control.disable_default_response = True + msg = zcl_f.GENERAL_COMMANDS[zcl_f.GeneralCommand.Report_Attributes].schema( + attribute_reports=attrs + ) + cluster.handle_message(hdr, msg) def get_zha_gateway(hass): @@ -96,13 +107,23 @@ async def send_attributes_report(hass, cluster: zigpy.zcl.Cluster, attributes: d This is to simulate the normal device communication that happens when a device is paired to the zigbee network. """ - attrs = [ - make_attribute(cluster.attridx.get(attr, attr), value) - for attr, value in attributes.items() - ] - hdr = make_zcl_header(zcl_f.Command.Report_Attributes) + attrs = [] + + for attrid, value in attributes.items(): + if isinstance(attrid, str): + attrid = cluster.attributes_by_name[attrid].id + else: + attrid = zigpy.types.uint16_t(attrid) + + attrs.append(make_attribute(attrid, value)) + + msg = zcl_f.GENERAL_COMMANDS[zcl_f.GeneralCommand.Report_Attributes].schema( + attribute_reports=attrs + ) + + hdr = make_zcl_header(zcl_f.GeneralCommand.Report_Attributes) hdr.frame_control.disable_default_response = True - cluster.handle_message(hdr, [attrs]) + cluster.handle_message(hdr, msg) await hass.async_block_till_done() diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index fd138567367..0e969b1b0f3 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -27,6 +27,20 @@ FIXTURE_GRP_ID = 0x1001 FIXTURE_GRP_NAME = "fixture group" +@pytest.fixture(scope="session", autouse=True) +def globally_load_quirks(): + """Load quirks automatically so that ZHA tests run deterministically in isolation. + + If portions of the ZHA test suite that do not happen to load quirks are run + independently, bugs can emerge that will show up only when more of the test suite is + run. + """ + + import zhaquirks + + zhaquirks.setup() + + @pytest.fixture def zigpy_app_controller(): """Zigpy ApplicationController fixture.""" diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index 4e97f35bf1d..dac9855148a 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -145,7 +145,7 @@ async def test_device_cluster_attributes(zha_client): msg = await zha_client.receive_json() attributes = msg["result"] - assert len(attributes) == 5 + assert len(attributes) == 7 for attribute in attributes: assert attribute[ID] is not None diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index 8eafdc451cc..79b8dbc6a71 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -130,7 +130,7 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock): 0x0201, 1, { - "local_temp", + "local_temperature", "occupied_cooling_setpoint", "occupied_heating_setpoint", "unoccupied_cooling_setpoint", @@ -586,13 +586,23 @@ async def test_zll_device_groups( cluster = zigpy_zll_device.endpoints[1].lightlink channel = zha_channels.lightlink.LightLink(cluster, channel_pool) + get_group_identifiers_rsp = zigpy.zcl.clusters.lightlink.LightLink.commands_by_name[ + "get_group_identifiers_rsp" + ].schema + with patch.object( - cluster, "command", AsyncMock(return_value=[1, 0, []]) + cluster, + "command", + AsyncMock( + return_value=get_group_identifiers_rsp( + total=0, start_index=0, group_info_records=[] + ) + ), ) as cmd_mock: await channel.async_configure() assert cmd_mock.await_count == 1 assert ( - cluster.server_commands[cmd_mock.await_args[0][0]][0] + cluster.server_commands[cmd_mock.await_args[0][0]].name == "get_group_identifiers" ) assert cluster.bind.call_count == 0 @@ -603,12 +613,18 @@ async def test_zll_device_groups( group_1 = zigpy.zcl.clusters.lightlink.GroupInfoRecord(0xABCD, 0x00) group_2 = zigpy.zcl.clusters.lightlink.GroupInfoRecord(0xAABB, 0x00) with patch.object( - cluster, "command", AsyncMock(return_value=[1, 0, [group_1, group_2]]) + cluster, + "command", + AsyncMock( + return_value=get_group_identifiers_rsp( + total=2, start_index=0, group_info_records=[group_1, group_2] + ) + ), ) as cmd_mock: await channel.async_configure() assert cmd_mock.await_count == 1 assert ( - cluster.server_commands[cmd_mock.await_args[0][0]][0] + cluster.server_commands[cmd_mock.await_args[0][0]].name == "get_group_identifiers" ) assert cluster.bind.call_count == 0 diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index 9f856ca1df6..fbf18ff9004 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -6,6 +6,7 @@ import pytest import zhaquirks.sinope.thermostat import zhaquirks.tuya.ts0601_trv import zigpy.profiles +import zigpy.types import zigpy.zcl.clusters from zigpy.zcl.clusters.hvac import Thermostat import zigpy.zcl.foundation as zcl_f @@ -162,8 +163,8 @@ ZCL_ATTR_PLUG = { "abs_max_heat_setpoint_limit": 3000, "abs_min_cool_setpoint_limit": 2000, "abs_max_cool_setpoint_limit": 4000, - "ctrl_seqe_of_oper": Thermostat.ControlSequenceOfOperation.Cooling_and_Heating, - "local_temp": None, + "ctrl_sequence_of_oper": Thermostat.ControlSequenceOfOperation.Cooling_and_Heating, + "local_temperature": None, "max_cool_setpoint_limit": 3900, "max_heat_setpoint_limit": 2900, "min_cool_setpoint_limit": 2100, @@ -268,7 +269,7 @@ def test_sequence_mappings(): assert Thermostat.SystemMode(HVAC_MODE_2_SYSTEM[hvac_mode]) is not None -async def test_climate_local_temp(hass, device_climate): +async def test_climate_local_temperature(hass, device_climate): """Test local temperature.""" thrm_cluster = device_climate.device.endpoints[1].thermostat @@ -517,7 +518,7 @@ async def test_hvac_modes(hass, device_climate_mock, seq_of_op, modes): """Test HVAC modes from sequence of operations.""" device_climate = await device_climate_mock( - CLIMATE, {"ctrl_seqe_of_oper": seq_of_op} + CLIMATE, {"ctrl_sequence_of_oper": seq_of_op} ) entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass) state = hass.states.get(entity_id) @@ -1119,7 +1120,7 @@ async def test_occupancy_reset(hass, device_climate_sinope): assert state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY await send_attributes_report( - hass, thrm_cluster, {"occupied_heating_setpoint": 1950} + hass, thrm_cluster, {"occupied_heating_setpoint": zigpy.types.uint16_t(1950)} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 73ab38c27ac..60a4fab25be 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -146,7 +146,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False assert cluster.request.call_args[0][1] == 0x01 - assert cluster.request.call_args[0][2] == () + assert cluster.request.call_args[0][2].command.name == "down_close" assert cluster.request.call_args[1]["expect_reply"] is True # open from UI @@ -159,7 +159,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False assert cluster.request.call_args[0][1] == 0x00 - assert cluster.request.call_args[0][2] == () + assert cluster.request.call_args[0][2].command.name == "up_open" assert cluster.request.call_args[1]["expect_reply"] is True # set position UI @@ -175,7 +175,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False assert cluster.request.call_args[0][1] == 0x05 - assert cluster.request.call_args[0][2] == (zigpy.types.uint8_t,) + assert cluster.request.call_args[0][2].command.name == "go_to_lift_percentage" assert cluster.request.call_args[0][3] == 53 assert cluster.request.call_args[1]["expect_reply"] is True @@ -189,7 +189,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False assert cluster.request.call_args[0][1] == 0x02 - assert cluster.request.call_args[0][2] == () + assert cluster.request.call_args[0][2].command.name == "stop" assert cluster.request.call_args[1]["expect_reply"] is True # test rejoin diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index 9953b6e9d15..93a50c77c90 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -120,7 +120,7 @@ async def test_devices( assert cluster_identify.request.call_args == mock.call( False, 64, - (zigpy.types.uint8_t, zigpy.types.uint8_t), + cluster_identify.commands_by_name["trigger_effect"].schema, 2, 0, expect_reply=True, diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 9c35215c889..4ac777f5d8e 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -4,7 +4,6 @@ from unittest.mock import AsyncMock, call, patch, sentinel import pytest import zigpy.profiles.zha as zha -import zigpy.types import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.lighting as lighting import zigpy.zcl.foundation as zcl_f @@ -336,7 +335,13 @@ async def async_test_on_off_from_hass(hass, cluster, entity_id): assert cluster.request.call_count == 1 assert cluster.request.await_count == 1 assert cluster.request.call_args == call( - False, ON, (), expect_reply=True, manufacturer=None, tries=1, tsn=None + False, + ON, + cluster.commands_by_name["on"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, ) await async_test_off_from_hass(hass, cluster, entity_id) @@ -353,7 +358,13 @@ async def async_test_off_from_hass(hass, cluster, entity_id): assert cluster.request.call_count == 1 assert cluster.request.await_count == 1 assert cluster.request.call_args == call( - False, OFF, (), expect_reply=True, manufacturer=None, tries=1, tsn=None + False, + OFF, + cluster.commands_by_name["off"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, ) @@ -373,7 +384,13 @@ async def async_test_level_on_off_from_hass( assert level_cluster.request.call_count == 0 assert level_cluster.request.await_count == 0 assert on_off_cluster.request.call_args == call( - False, ON, (), expect_reply=True, manufacturer=None, tries=1, tsn=None + False, + ON, + on_off_cluster.commands_by_name["on"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, ) on_off_cluster.request.reset_mock() level_cluster.request.reset_mock() @@ -389,12 +406,18 @@ async def async_test_level_on_off_from_hass( assert level_cluster.request.call_count == 1 assert level_cluster.request.await_count == 1 assert on_off_cluster.request.call_args == call( - False, ON, (), expect_reply=True, manufacturer=None, tries=1, tsn=None + False, + ON, + on_off_cluster.commands_by_name["on"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, ) assert level_cluster.request.call_args == call( False, 4, - (zigpy.types.uint8_t, zigpy.types.uint16_t), + level_cluster.commands_by_name["move_to_level_with_on_off"].schema, 254, 100.0, expect_reply=True, @@ -419,7 +442,7 @@ async def async_test_level_on_off_from_hass( assert level_cluster.request.call_args == call( False, 4, - (zigpy.types.uint8_t, zigpy.types.uint16_t), + level_cluster.commands_by_name["move_to_level_with_on_off"].schema, 10, 1, expect_reply=True, @@ -462,7 +485,7 @@ async def async_test_flash_from_hass(hass, cluster, entity_id, flash): assert cluster.request.call_args == call( False, 64, - (zigpy.types.uint8_t, zigpy.types.uint8_t), + cluster.commands_by_name["trigger_effect"].schema, FLASH_EFFECTS[flash], 0, expect_reply=True, diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index c4e66e98098..03a88c3560e 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -307,7 +307,7 @@ async def async_test_device_temperature(hass, cluster, entity_id): "metering_device_type": 0x00, "multiplier": 1, "status": 0x00, - "summa_formatting": 0b1_0111_010, + "summation_formatting": 0b1_0111_010, "unit_of_measure": 0x01, }, {"instaneneous_demand"}, @@ -814,7 +814,7 @@ async def test_se_summation_uom( "metering_device_type": 0x00, "multiplier": 1, "status": 0x00, - "summa_formatting": 0b1_0111_010, + "summation_formatting": 0b1_0111_010, "unit_of_measure": raw_uom, } await zha_device_joined(zigpy_device) diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index c5cdf1a96f1..a624e5f2c73 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -141,7 +141,13 @@ async def test_switch(hass, zha_device_joined_restored, zigpy_device): ) assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args == call( - False, ON, (), expect_reply=True, manufacturer=None, tries=1, tsn=None + False, + ON, + cluster.commands_by_name["on"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, ) # turn off from HA @@ -155,7 +161,13 @@ async def test_switch(hass, zha_device_joined_restored, zigpy_device): ) assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args == call( - False, OFF, (), expect_reply=True, manufacturer=None, tries=1, tsn=None + False, + OFF, + cluster.commands_by_name["off"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, ) # test joining a new switch to the network and HA @@ -224,7 +236,13 @@ async def test_zha_group_switch_entity( ) assert len(group_cluster_on_off.request.mock_calls) == 1 assert group_cluster_on_off.request.call_args == call( - False, ON, (), expect_reply=True, manufacturer=None, tries=1, tsn=None + False, + ON, + group_cluster_on_off.commands_by_name["on"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, ) assert hass.states.get(entity_id).state == STATE_ON @@ -239,7 +257,13 @@ async def test_zha_group_switch_entity( ) assert len(group_cluster_on_off.request.mock_calls) == 1 assert group_cluster_on_off.request.call_args == call( - False, OFF, (), expect_reply=True, manufacturer=None, tries=1, tsn=None + False, + OFF, + group_cluster_on_off.commands_by_name["off"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, ) assert hass.states.get(entity_id).state == STATE_OFF