Bump zigpy to 0.44.1 and zha-quirks to 0.0.69 (#68921)

* Make unit tests pass

* Flip response type check to not rely on it being a list
https://github.com/zigpy/zigpy/pull/716#issuecomment-1025236190

* Bump zigpy and quirks versions to ZCLR8 releases

* Fix renamed zigpy cluster attributes

* Handle the default response for ZLL `get_group_identifiers`

* Add more error context to `stage failed` errors

* Fix unit test returning lists as ZCL request responses

* Always load quirks when testing ZHA

* Bump zha-quirks to 0.0.69
pull/68998/head
puddly 2022-03-31 11:26:27 -04:00 committed by GitHub
parent 398db35334
commit 0f6296e4b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 248 additions and 122 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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":

View File

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

View File

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

View File

@ -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},
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"

View File

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

View File

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

View File

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

View File

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

View File

@ -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."""

View File

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

View File

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

View File

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

View File

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

View File

@ -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,

View File

@ -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,

View File

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

View File

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