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
This commit is contained in:
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: if attributes is not None:
for attr_id, attr in attributes.items(): 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( _LOGGER.debug(
"Requested attributes for: %s: %s, %s: '%s', %s: %s, %s: %s", "Requested attributes for: %s: %s, %s: '%s', %s: %s, %s: %s",
ATTR_CLUSTER_ID, ATTR_CLUSTER_ID,
@ -700,7 +700,7 @@ async def websocket_device_cluster_commands(
{ {
TYPE: CLIENT, TYPE: CLIENT,
ID: cmd_id, ID: cmd_id,
ATTR_NAME: cmd[0], ATTR_NAME: cmd.name,
} }
) )
for cmd_id, cmd in commands[CLUSTER_COMMANDS_SERVER].items(): for cmd_id, cmd in commands[CLUSTER_COMMANDS_SERVER].items():
@ -708,7 +708,7 @@ async def websocket_device_cluster_commands(
{ {
TYPE: CLUSTER_COMMAND_SERVER, TYPE: CLUSTER_COMMAND_SERVER,
ID: cmd_id, ID: cmd_id,
ATTR_NAME: cmd[0], ATTR_NAME: cmd.name,
} }
) )
_LOGGER.debug( _LOGGER.debug(

View File

@ -161,9 +161,9 @@ class Thermostat(ZhaEntity, ClimateEntity):
@property @property
def current_temperature(self): def current_temperature(self):
"""Return the current temperature.""" """Return the current temperature."""
if self._thrm.local_temp is None: if self._thrm.local_temperature is None:
return None return None
return self._thrm.local_temp / ZCL_TEMP return self._thrm.local_temperature / ZCL_TEMP
@property @property
def extra_state_attributes(self): def extra_state_attributes(self):
@ -272,7 +272,7 @@ class Thermostat(ZhaEntity, ClimateEntity):
@property @property
def hvac_modes(self) -> tuple[str, ...]: def hvac_modes(self) -> tuple[str, ...]:
"""Return the list of available HVAC operation modes.""" """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 @property
def precision(self): def precision(self):

View File

@ -346,7 +346,9 @@ class ChannelPool:
results = await asyncio.gather(*tasks, return_exceptions=True) results = await asyncio.gather(*tasks, return_exceptions=True)
for channel, outcome in zip(channels, results): for channel, outcome in zip(channels, results):
if isinstance(outcome, Exception): 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 continue
channel.debug("'%s' stage succeeded", func_name) channel.debug("'%s' stage succeeded", func_name)

View File

@ -8,7 +8,7 @@ import logging
from typing import Any from typing import Any
import zigpy.exceptions import zigpy.exceptions
from zigpy.zcl.foundation import Status from zigpy.zcl.foundation import ConfigureReportingResponseRecord, Status
from homeassistant.const import ATTR_COMMAND from homeassistant.const import ATTR_COMMAND
from homeassistant.core import callback from homeassistant.core import callback
@ -111,7 +111,7 @@ class ZigbeeChannel(LogMixin):
if not hasattr(self, "_value_attribute") and self.REPORT_CONFIG: if not hasattr(self, "_value_attribute") and self.REPORT_CONFIG:
attr = self.REPORT_CONFIG[0].get("attr") attr = self.REPORT_CONFIG[0].get("attr")
if isinstance(attr, str): if isinstance(attr, str):
self.value_attribute = self.cluster.attridx.get(attr) self.value_attribute = self.cluster.attributes_by_name.get(attr)
else: else:
self.value_attribute = attr self.value_attribute = attr
self._status = ChannelStatus.CREATED self._status = ChannelStatus.CREATED
@ -260,7 +260,7 @@ class ZigbeeChannel(LogMixin):
self, attrs: dict[int | str, tuple], res: list | tuple self, attrs: dict[int | str, tuple], res: list | tuple
) -> None: ) -> None:
"""Parse configure reporting result.""" """Parse configure reporting result."""
if not isinstance(res, list): if isinstance(res, (Exception, ConfigureReportingResponseRecord)):
# assume default response # assume default response
self.debug( self.debug(
"attr reporting for '%s' on '%s': %s", "attr reporting for '%s' on '%s': %s",
@ -345,7 +345,7 @@ class ZigbeeChannel(LogMixin):
self.async_send_signal( self.async_send_signal(
f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}",
attrid, attrid,
self.cluster.attributes.get(attrid, [attrid])[0], self._get_attribute_name(attrid),
value, value,
) )
@ -368,6 +368,12 @@ class ZigbeeChannel(LogMixin):
async def async_update(self): async def async_update(self):
"""Retrieve latest state from cluster.""" """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): async def get_attribute_value(self, attribute, from_cache=True):
"""Get the value for an attribute.""" """Get the value for an attribute."""
manufacturer = None manufacturer = None
@ -421,11 +427,11 @@ class ZigbeeChannel(LogMixin):
get_attributes = partialmethod(_get_attributes, False) get_attributes = partialmethod(_get_attributes, False)
def log(self, level, msg, *args): def log(self, level, msg, *args, **kwargs):
"""Log a message.""" """Log a message."""
msg = f"[%s:%s]: {msg}" msg = f"[%s:%s]: {msg}"
args = (self._ch_pool.nwk, self._id) + args args = (self._ch_pool.nwk, self._id) + args
_LOGGER.log(level, msg, *args) _LOGGER.log(level, msg, *args, **kwargs)
def __getattr__(self, name): def __getattr__(self, name):
"""Get attribute or a decorated cluster command.""" """Get attribute or a decorated cluster command."""
@ -479,11 +485,11 @@ class ZDOChannel(LogMixin):
"""Configure channel.""" """Configure channel."""
self._status = ChannelStatus.CONFIGURED self._status = ChannelStatus.CONFIGURED
def log(self, level, msg, *args): def log(self, level, msg, *args, **kwargs):
"""Log a message.""" """Log a message."""
msg = f"[%s:ZDO](%s): {msg}" msg = f"[%s:ZDO](%s): {msg}"
args = (self._zha_device.nwk, self._zha_device.model) + args args = (self._zha_device.nwk, self._zha_device.model) + args
_LOGGER.log(level, msg, *args) _LOGGER.log(level, msg, *args, **kwargs)
class ClientChannel(ZigbeeChannel): class ClientChannel(ZigbeeChannel):
@ -492,13 +498,17 @@ class ClientChannel(ZigbeeChannel):
@callback @callback
def attribute_updated(self, attrid, value): def attribute_updated(self, attrid, value):
"""Handle an attribute updated on this cluster.""" """Handle an attribute updated on this cluster."""
try:
attr_name = self._cluster.attributes[attrid].name
except KeyError:
attr_name = "Unknown"
self.zha_send_event( self.zha_send_event(
SIGNAL_ATTR_UPDATED, SIGNAL_ATTR_UPDATED,
{ {
ATTR_ATTRIBUTE_ID: attrid, ATTR_ATTRIBUTE_ID: attrid,
ATTR_ATTRIBUTE_NAME: self._cluster.attributes.get(attrid, ["Unknown"])[ ATTR_ATTRIBUTE_NAME: attr_name,
0
],
ATTR_VALUE: value, ATTR_VALUE: value,
}, },
) )
@ -510,4 +520,4 @@ class ClientChannel(ZigbeeChannel):
self._cluster.server_commands is not None self._cluster.server_commands is not None
and self._cluster.server_commands.get(command_id) 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 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": if command_name == "operation_event_notification":
self.zha_send_event( self.zha_send_event(
command_name, command_name,
@ -47,7 +48,7 @@ class DoorLockChannel(ZigbeeChannel):
@callback @callback
def attribute_updated(self, attrid, value): def attribute_updated(self, attrid, value):
"""Handle attribute update from lock cluster.""" """Handle attribute update from lock cluster."""
attr_name = self.cluster.attributes.get(attrid, [attrid])[0] attr_name = self._get_attribute_name(attrid)
self.debug( self.debug(
"Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value
) )
@ -140,7 +141,7 @@ class WindowCovering(ZigbeeChannel):
@callback @callback
def attribute_updated(self, attrid, value): def attribute_updated(self, attrid, value):
"""Handle attribute update from window_covering cluster.""" """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( self.debug(
"Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value "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: except zigpy.exceptions.ZigbeeException as ex:
self.error("Could not set value: %s", ex) self.error("Could not set value: %s", ex)
return False return False
if isinstance(res, list) and all( if not isinstance(res, Exception) and all(
record.status == Status.SUCCESS for record in res[0] record.status == Status.SUCCESS for record in res[0]
): ):
return True return True
@ -380,7 +380,11 @@ class Ota(ZigbeeChannel):
self, tsn: int, command_id: int, args: list[Any] | None self, tsn: int, command_id: int, args: list[Any] | None
) -> None: ) -> None:
"""Handle OTA commands.""" """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] signal_id = self._ch_pool.unique_id.split("-")[0]
if cmd_name == "query_next_image": if cmd_name == "query_next_image":
self.async_send_signal(SIGNAL_UPDATE_DEVICE.format(signal_id), args[3]) 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 self, tsn: int, command_id: int, args: list[Any] | None
) -> None: ) -> None:
"""Handle commands received to this cluster.""" """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.debug("Received %s tsn command '%s': %s", tsn, cmd_name, args)
self.zha_send_event(cmd_name, args) self.zha_send_event(cmd_name, args)
if cmd_name == "checkin": if cmd_name == "checkin":

View File

@ -70,7 +70,7 @@ class FanChannel(ZigbeeChannel):
@callback @callback
def attribute_updated(self, attrid: int, value: Any) -> None: def attribute_updated(self, attrid: int, value: Any) -> None:
"""Handle attribute update from fan cluster.""" """Handle attribute update from fan cluster."""
attr_name = self.cluster.attributes.get(attrid, [attrid])[0] attr_name = self._get_attribute_name(attrid)
self.debug( self.debug(
"Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value
) )
@ -90,7 +90,7 @@ class ThermostatChannel(ZigbeeChannel):
"""Thermostat channel.""" """Thermostat channel."""
REPORT_CONFIG = ( 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_cooling_setpoint", "config": REPORT_CONFIG_CLIMATE},
{"attr": "occupied_heating_setpoint", "config": REPORT_CONFIG_CLIMATE}, {"attr": "occupied_heating_setpoint", "config": REPORT_CONFIG_CLIMATE},
{"attr": "unoccupied_cooling_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_max_heat_setpoint_limit": True,
"abs_min_cool_setpoint_limit": True, "abs_min_cool_setpoint_limit": True,
"abs_max_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_cool_setpoint_limit": True,
"max_heat_setpoint_limit": True, "max_heat_setpoint_limit": True,
"min_cool_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) return self.cluster.get("abs_min_heat_setpoint_limit", 700)
@property @property
def ctrl_seqe_of_oper(self) -> int: def ctrl_sequence_of_oper(self) -> int:
"""Control Sequence of operations attribute.""" """Control Sequence of operations attribute."""
return self.cluster.get("ctrl_seqe_of_oper", 0xFF) return self.cluster.get("ctrl_sequence_of_oper", 0xFF)
@property @property
def max_cool_setpoint_limit(self) -> int: def max_cool_setpoint_limit(self) -> int:
@ -172,9 +172,9 @@ class ThermostatChannel(ZigbeeChannel):
return sp_limit return sp_limit
@property @property
def local_temp(self) -> int | None: def local_temperature(self) -> int | None:
"""Thermostat temperature.""" """Thermostat temperature."""
return self.cluster.get("local_temp") return self.cluster.get("local_temperature")
@property @property
def occupancy(self) -> int | None: def occupancy(self) -> int | None:
@ -229,7 +229,7 @@ class ThermostatChannel(ZigbeeChannel):
@callback @callback
def attribute_updated(self, attrid, value): def attribute_updated(self, attrid, value):
"""Handle attribute update cluster.""" """Handle attribute update cluster."""
attr_name = self.cluster.attributes.get(attrid, [attrid])[0] attr_name = self._get_attribute_name(attrid)
self.debug( self.debug(
"Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value
) )
@ -300,7 +300,7 @@ class ThermostatChannel(ZigbeeChannel):
@staticmethod @staticmethod
def check_result(res: list) -> bool: def check_result(res: list) -> bool:
"""Normalize the result.""" """Normalize the result."""
if not isinstance(res, list): if isinstance(res, Exception):
return False return False
return all(record.status == Status.SUCCESS for record in res[0]) return all(record.status == Status.SUCCESS for record in res[0])

View File

@ -3,6 +3,7 @@ import asyncio
import zigpy.exceptions import zigpy.exceptions
from zigpy.zcl.clusters import lightlink from zigpy.zcl.clusters import lightlink
from zigpy.zcl.foundation import GENERAL_COMMANDS, GeneralCommand
from .. import registries from .. import registries
from .base import ChannelStatus, ZigbeeChannel from .base import ChannelStatus, ZigbeeChannel
@ -30,11 +31,16 @@ class LightLink(ZigbeeChannel):
return return
try: 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: except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as exc:
self.warning("Couldn't get list of groups: %s", str(exc)) self.warning("Couldn't get list of groups: %s", str(exc))
return return
if isinstance(rsp, GENERAL_COMMANDS[GeneralCommand.Default_Response].schema):
groups = []
else:
groups = rsp.group_info_records
if groups: if groups:
for group in groups: for group in groups:
self.debug("Adding coordinator to 0x%04x group id", group.group_id) 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: def cluster_command(self, tsn, command_id, args) -> None:
"""Handle commands received to this cluster.""" """Handle commands received to this cluster."""
self.warning( 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) self.command_map[command_id](*args)
@ -94,7 +94,7 @@ class IasAce(ZigbeeChannel):
mode = AceCluster.ArmMode(arm_mode) mode = AceCluster.ArmMode(arm_mode)
self.zha_send_event( 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": mode.value,
"arm_mode_description": mode.name, "arm_mode_description": mode.name,
@ -190,7 +190,7 @@ class IasAce(ZigbeeChannel):
def _bypass(self, zone_list, code) -> None: def _bypass(self, zone_list, code) -> None:
"""Handle the IAS ACE bypass command.""" """Handle the IAS ACE bypass command."""
self.zha_send_event( 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}, {"zone_list": zone_list, "code": code},
) )

View File

@ -65,7 +65,7 @@ class Metering(ZigbeeChannel):
"divisor": True, "divisor": True,
"metering_device_type": True, "metering_device_type": True,
"multiplier": True, "multiplier": True,
"summa_formatting": True, "summation_formatting": True,
"unit_of_measure": True, "unit_of_measure": True,
} }
@ -159,7 +159,7 @@ class Metering(ZigbeeChannel):
self._format_spec = self.get_formatting(fmting) self._format_spec = self.get_formatting(fmting)
fmting = self.cluster.get( fmting = self.cluster.get(
"summa_formatting", 0xF9 "summation_formatting", 0xF9
) # 1 digit to the right, 15 digits to the left ) # 1 digit to the right, 15 digits to the left
self._summa_format = self.get_formatting(fmting) self._summa_format = self.get_formatting(fmting)

View File

@ -783,8 +783,8 @@ class ZHADevice(LogMixin):
fmt = f"{log_msg[1]} completed: %s" fmt = f"{log_msg[1]} completed: %s"
zdo.debug(fmt, *(log_msg[2] + (outcome,))) 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.""" """Log a message."""
msg = f"[%s](%s): {msg}" msg = f"[%s](%s): {msg}"
args = (self.nwk, self.model) + args 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), 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.""" """Log a message."""
msg = f"[%s](%s): {msg}" msg = f"[%s](%s): {msg}"
args = (f"0x{self._zha_group.group_id:04x}", self.endpoint_id) + args 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): class ZHAGroup(LogMixin):
@ -224,8 +224,8 @@ class ZHAGroup(LogMixin):
group_info["members"] = [member.member_info for member in self.members] group_info["members"] = [member.member_info for member in self.members]
return group_info 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.""" """Log a message."""
msg = f"[%s](%s): {msg}" msg = f"[%s](%s): {msg}"
args = (self.name, self.group_id) + args 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: class LogMixin:
"""Log helper.""" """Log helper."""
def log(self, level, msg, *args): def log(self, level, msg, *args, **kwargs):
"""Log with level.""" """Log with level."""
raise NotImplementedError raise NotImplementedError
def debug(self, msg, *args): def debug(self, msg, *args, **kwargs):
"""Debug level log.""" """Debug level log."""
return self.log(logging.DEBUG, msg, *args) return self.log(logging.DEBUG, msg, *args)
def info(self, msg, *args): def info(self, msg, *args, **kwargs):
"""Info level log.""" """Info level log."""
return self.log(logging.INFO, msg, *args) return self.log(logging.INFO, msg, *args)
def warning(self, msg, *args): def warning(self, msg, *args, **kwargs):
"""Warning method log.""" """Warning method log."""
return self.log(logging.WARNING, msg, *args) return self.log(logging.WARNING, msg, *args)
def error(self, msg, *args): def error(self, msg, *args, **kwargs):
"""Error level log.""" """Error level log."""
return self.log(logging.ERROR, msg, *args) return self.log(logging.ERROR, msg, *args)

View File

@ -133,20 +133,20 @@ class ZhaCover(ZhaEntity, CoverEntity):
async def async_open_cover(self, **kwargs): async def async_open_cover(self, **kwargs):
"""Open the window cover.""" """Open the window cover."""
res = await self._cover_channel.up_open() 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) self.async_update_state(STATE_OPENING)
async def async_close_cover(self, **kwargs): async def async_close_cover(self, **kwargs):
"""Close the window cover.""" """Close the window cover."""
res = await self._cover_channel.down_close() 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) self.async_update_state(STATE_CLOSING)
async def async_set_cover_position(self, **kwargs): async def async_set_cover_position(self, **kwargs):
"""Move the roller shutter to a specific position.""" """Move the roller shutter to a specific position."""
new_pos = kwargs[ATTR_POSITION] new_pos = kwargs[ATTR_POSITION]
res = await self._cover_channel.go_to_lift_percentage(100 - new_pos) 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( self.async_update_state(
STATE_CLOSING if new_pos < self._current_position else STATE_OPENING 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): async def async_stop_cover(self, **kwargs):
"""Stop the window cover.""" """Stop the window cover."""
res = await self._cover_channel.stop() 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._state = STATE_OPEN if self._current_position > 0 else STATE_CLOSED
self.async_write_ha_state() self.async_write_ha_state()
@ -250,7 +250,7 @@ class Shade(ZhaEntity, CoverEntity):
async def async_open_cover(self, **kwargs): async def async_open_cover(self, **kwargs):
"""Open the window cover.""" """Open the window cover."""
res = await self._on_off_channel.on() 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) self.debug("couldn't open cover: %s", res)
return return
@ -260,7 +260,7 @@ class Shade(ZhaEntity, CoverEntity):
async def async_close_cover(self, **kwargs): async def async_close_cover(self, **kwargs):
"""Close the window cover.""" """Close the window cover."""
res = await self._on_off_channel.off() 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) self.debug("couldn't open cover: %s", res)
return return
@ -274,7 +274,7 @@ class Shade(ZhaEntity, CoverEntity):
new_pos * 255 / 100, 1 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) self.debug("couldn't set cover's position: %s", res)
return return
@ -284,7 +284,7 @@ class Shade(ZhaEntity, CoverEntity):
async def async_stop_cover(self, **kwargs) -> None: async def async_stop_cover(self, **kwargs) -> None:
"""Stop the cover.""" """Stop the cover."""
res = await self._level_channel.stop() 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) self.debug("couldn't stop cover: %s", res)
return return

View File

@ -139,11 +139,11 @@ class BaseZhaEntity(LogMixin, entity.Entity):
) )
self._unsubs.append(unsub) self._unsubs.append(unsub)
def log(self, level: int, msg: str, *args): def log(self, level: int, msg: str, *args, **kwargs):
"""Log a message.""" """Log a message."""
msg = f"%s: {msg}" msg = f"%s: {msg}"
args = (self.entity_id,) + args args = (self.entity_id,) + args
_LOGGER.log(level, msg, *args) _LOGGER.log(level, msg, *args, **kwargs)
class ZhaEntity(BaseZhaEntity, RestoreEntity): class ZhaEntity(BaseZhaEntity, RestoreEntity):

View File

@ -243,7 +243,7 @@ class BaseLight(LogMixin, light.LightEntity):
level, duration level, duration
) )
t_log["move_to_level_with_on_off"] = result 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) self.debug("turned on: %s", t_log)
return return
self._state = bool(level) 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. # we should call the on command on the on_off cluster if brightness is not 0.
result = await self._on_off_channel.on() result = await self._on_off_channel.on()
t_log["on_off"] = result 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) self.debug("turned on: %s", t_log)
return return
self._state = True self._state = True
@ -266,7 +266,7 @@ class BaseLight(LogMixin, light.LightEntity):
temperature = kwargs[light.ATTR_COLOR_TEMP] temperature = kwargs[light.ATTR_COLOR_TEMP]
result = await self._color_channel.move_to_color_temp(temperature, duration) result = await self._color_channel.move_to_color_temp(temperature, duration)
t_log["move_to_color_temp"] = result 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) self.debug("turned on: %s", t_log)
return return
self._color_temp = temperature self._color_temp = temperature
@ -282,7 +282,7 @@ class BaseLight(LogMixin, light.LightEntity):
int(xy_color[0] * 65535), int(xy_color[1] * 65535), duration int(xy_color[0] * 65535), int(xy_color[1] * 65535), duration
) )
t_log["move_to_color"] = result 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) self.debug("turned on: %s", t_log)
return return
self._hs_color = hs_color self._hs_color = hs_color
@ -340,7 +340,7 @@ class BaseLight(LogMixin, light.LightEntity):
else: else:
result = await self._on_off_channel.off() result = await self._on_off_channel.off()
self.debug("turned off: %s", result) 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 return
self._state = False self._state = False

View File

@ -122,7 +122,7 @@ class ZhaDoorLock(ZhaEntity, LockEntity):
async def async_lock(self, **kwargs): async def async_lock(self, **kwargs):
"""Lock the lock.""" """Lock the lock."""
result = await self._doorlock_channel.lock_door() 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) self.error("Error with lock_door: %s", result)
return return
self.async_write_ha_state() self.async_write_ha_state()
@ -130,7 +130,7 @@ class ZhaDoorLock(ZhaEntity, LockEntity):
async def async_unlock(self, **kwargs): async def async_unlock(self, **kwargs):
"""Unlock the lock.""" """Unlock the lock."""
result = await self._doorlock_channel.unlock_door() 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) self.error("Error with unlock_door: %s", result)
return return
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -7,9 +7,9 @@
"bellows==0.29.0", "bellows==0.29.0",
"pyserial==3.5", "pyserial==3.5",
"pyserial-asyncio==0.6", "pyserial-asyncio==0.6",
"zha-quirks==0.0.67", "zha-quirks==0.0.69",
"zigpy-deconz==0.14.0", "zigpy-deconz==0.14.0",
"zigpy==0.43.0", "zigpy==0.44.1",
"zigpy-xbee==0.14.0", "zigpy-xbee==0.14.0",
"zigpy-zigate==0.8.0", "zigpy-zigate==0.8.0",
"zigpy-znp==0.7.0" "zigpy-znp==0.7.0"

View File

@ -65,7 +65,7 @@ class BaseSwitch(SwitchEntity):
async def async_turn_on(self, **kwargs) -> None: async def async_turn_on(self, **kwargs) -> None:
"""Turn the entity on.""" """Turn the entity on."""
result = await self._on_off_channel.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 return
self._state = True self._state = True
self.async_write_ha_state() self.async_write_ha_state()
@ -73,7 +73,7 @@ class BaseSwitch(SwitchEntity):
async def async_turn_off(self, **kwargs) -> None: async def async_turn_off(self, **kwargs) -> None:
"""Turn the entity off.""" """Turn the entity off."""
result = await self._on_off_channel.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 return
self._state = False self._state = False
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -2469,7 +2469,7 @@ zengge==0.2
zeroconf==0.38.4 zeroconf==0.38.4
# homeassistant.components.zha # homeassistant.components.zha
zha-quirks==0.0.67 zha-quirks==0.0.69
# homeassistant.components.zhong_hong # homeassistant.components.zhong_hong
zhong_hong_hvac==1.0.9 zhong_hong_hvac==1.0.9
@ -2490,7 +2490,7 @@ zigpy-zigate==0.8.0
zigpy-znp==0.7.0 zigpy-znp==0.7.0
# homeassistant.components.zha # homeassistant.components.zha
zigpy==0.43.0 zigpy==0.44.1
# homeassistant.components.zoneminder # homeassistant.components.zoneminder
zm-py==0.5.2 zm-py==0.5.2

View File

@ -1595,7 +1595,7 @@ youless-api==0.16
zeroconf==0.38.4 zeroconf==0.38.4
# homeassistant.components.zha # homeassistant.components.zha
zha-quirks==0.0.67 zha-quirks==0.0.69
# homeassistant.components.zha # homeassistant.components.zha
zigpy-deconz==0.14.0 zigpy-deconz==0.14.0
@ -1610,7 +1610,7 @@ zigpy-zigate==0.8.0
zigpy-znp==0.7.0 zigpy-znp==0.7.0
# homeassistant.components.zha # homeassistant.components.zha
zigpy==0.43.0 zigpy==0.44.1
# homeassistant.components.zwave_js # homeassistant.components.zwave_js
zwave-js-server-python==0.35.2 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) value = cluster.PLUGGED_ATTR_READS.get(attr_id)
if value is None: if value is None:
# try converting attr_id to attr_name and lookup the plugs again # try converting attr_id to attr_name and lookup the plugs again
attr_name = cluster.attributes.get(attr_id) attr = cluster.attributes.get(attr_id)
value = attr_name and cluster.PLUGGED_ATTR_READS.get(attr_name[0])
if attr is not None:
value = cluster.PLUGGED_ATTR_READS.get(attr.name)
if value is not None: if value is not None:
result.append( result.append(
zcl_f.ReadAttributeRecord( zcl_f.ReadAttributeRecord(
@ -58,14 +60,23 @@ def patch_cluster(cluster):
def update_attribute_cache(cluster): def update_attribute_cache(cluster):
"""Update attribute cache based on plugged attributes.""" """Update attribute cache based on plugged attributes."""
if cluster.PLUGGED_ATTR_READS: if not cluster.PLUGGED_ATTR_READS:
attrs = [ return
make_attribute(cluster.attridx.get(attr, attr), value)
for attr, value in cluster.PLUGGED_ATTR_READS.items() attrs = []
] for attrid, value in cluster.PLUGGED_ATTR_READS.items():
hdr = make_zcl_header(zcl_f.Command.Report_Attributes) if isinstance(attrid, str):
hdr.frame_control.disable_default_response = True attrid = cluster.attributes_by_name[attrid].id
cluster.handle_message(hdr, [attrs]) 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): 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 This is to simulate the normal device communication that happens when a
device is paired to the zigbee network. device is paired to the zigbee network.
""" """
attrs = [ attrs = []
make_attribute(cluster.attridx.get(attr, attr), value)
for attr, value in attributes.items() for attrid, value in attributes.items():
] if isinstance(attrid, str):
hdr = make_zcl_header(zcl_f.Command.Report_Attributes) 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 hdr.frame_control.disable_default_response = True
cluster.handle_message(hdr, [attrs]) cluster.handle_message(hdr, msg)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -27,6 +27,20 @@ FIXTURE_GRP_ID = 0x1001
FIXTURE_GRP_NAME = "fixture group" 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 @pytest.fixture
def zigpy_app_controller(): def zigpy_app_controller():
"""Zigpy ApplicationController fixture.""" """Zigpy ApplicationController fixture."""

View File

@ -145,7 +145,7 @@ async def test_device_cluster_attributes(zha_client):
msg = await zha_client.receive_json() msg = await zha_client.receive_json()
attributes = msg["result"] attributes = msg["result"]
assert len(attributes) == 5 assert len(attributes) == 7
for attribute in attributes: for attribute in attributes:
assert attribute[ID] is not None assert attribute[ID] is not None

View File

@ -130,7 +130,7 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock):
0x0201, 0x0201,
1, 1,
{ {
"local_temp", "local_temperature",
"occupied_cooling_setpoint", "occupied_cooling_setpoint",
"occupied_heating_setpoint", "occupied_heating_setpoint",
"unoccupied_cooling_setpoint", "unoccupied_cooling_setpoint",
@ -586,13 +586,23 @@ async def test_zll_device_groups(
cluster = zigpy_zll_device.endpoints[1].lightlink cluster = zigpy_zll_device.endpoints[1].lightlink
channel = zha_channels.lightlink.LightLink(cluster, channel_pool) 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( 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: ) as cmd_mock:
await channel.async_configure() await channel.async_configure()
assert cmd_mock.await_count == 1 assert cmd_mock.await_count == 1
assert ( assert (
cluster.server_commands[cmd_mock.await_args[0][0]][0] cluster.server_commands[cmd_mock.await_args[0][0]].name
== "get_group_identifiers" == "get_group_identifiers"
) )
assert cluster.bind.call_count == 0 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_1 = zigpy.zcl.clusters.lightlink.GroupInfoRecord(0xABCD, 0x00)
group_2 = zigpy.zcl.clusters.lightlink.GroupInfoRecord(0xAABB, 0x00) group_2 = zigpy.zcl.clusters.lightlink.GroupInfoRecord(0xAABB, 0x00)
with patch.object( 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: ) as cmd_mock:
await channel.async_configure() await channel.async_configure()
assert cmd_mock.await_count == 1 assert cmd_mock.await_count == 1
assert ( assert (
cluster.server_commands[cmd_mock.await_args[0][0]][0] cluster.server_commands[cmd_mock.await_args[0][0]].name
== "get_group_identifiers" == "get_group_identifiers"
) )
assert cluster.bind.call_count == 0 assert cluster.bind.call_count == 0

View File

@ -6,6 +6,7 @@ import pytest
import zhaquirks.sinope.thermostat import zhaquirks.sinope.thermostat
import zhaquirks.tuya.ts0601_trv import zhaquirks.tuya.ts0601_trv
import zigpy.profiles import zigpy.profiles
import zigpy.types
import zigpy.zcl.clusters import zigpy.zcl.clusters
from zigpy.zcl.clusters.hvac import Thermostat from zigpy.zcl.clusters.hvac import Thermostat
import zigpy.zcl.foundation as zcl_f import zigpy.zcl.foundation as zcl_f
@ -162,8 +163,8 @@ ZCL_ATTR_PLUG = {
"abs_max_heat_setpoint_limit": 3000, "abs_max_heat_setpoint_limit": 3000,
"abs_min_cool_setpoint_limit": 2000, "abs_min_cool_setpoint_limit": 2000,
"abs_max_cool_setpoint_limit": 4000, "abs_max_cool_setpoint_limit": 4000,
"ctrl_seqe_of_oper": Thermostat.ControlSequenceOfOperation.Cooling_and_Heating, "ctrl_sequence_of_oper": Thermostat.ControlSequenceOfOperation.Cooling_and_Heating,
"local_temp": None, "local_temperature": None,
"max_cool_setpoint_limit": 3900, "max_cool_setpoint_limit": 3900,
"max_heat_setpoint_limit": 2900, "max_heat_setpoint_limit": 2900,
"min_cool_setpoint_limit": 2100, "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 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.""" """Test local temperature."""
thrm_cluster = device_climate.device.endpoints[1].thermostat 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.""" """Test HVAC modes from sequence of operations."""
device_climate = await device_climate_mock( 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) entity_id = await find_entity_id(Platform.CLIMATE, device_climate, hass)
state = hass.states.get(entity_id) 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 assert state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY
await send_attributes_report( 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) state = hass.states.get(entity_id)
assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE 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_count == 1
assert cluster.request.call_args[0][0] is False assert cluster.request.call_args[0][0] is False
assert cluster.request.call_args[0][1] == 0x01 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 assert cluster.request.call_args[1]["expect_reply"] is True
# open from UI # 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_count == 1
assert cluster.request.call_args[0][0] is False assert cluster.request.call_args[0][0] is False
assert cluster.request.call_args[0][1] == 0x00 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 assert cluster.request.call_args[1]["expect_reply"] is True
# set position UI # 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_count == 1
assert cluster.request.call_args[0][0] is False assert cluster.request.call_args[0][0] is False
assert cluster.request.call_args[0][1] == 0x05 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[0][3] == 53
assert cluster.request.call_args[1]["expect_reply"] is True 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_count == 1
assert cluster.request.call_args[0][0] is False assert cluster.request.call_args[0][0] is False
assert cluster.request.call_args[0][1] == 0x02 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 assert cluster.request.call_args[1]["expect_reply"] is True
# test rejoin # test rejoin

View File

@ -120,7 +120,7 @@ async def test_devices(
assert cluster_identify.request.call_args == mock.call( assert cluster_identify.request.call_args == mock.call(
False, False,
64, 64,
(zigpy.types.uint8_t, zigpy.types.uint8_t), cluster_identify.commands_by_name["trigger_effect"].schema,
2, 2,
0, 0,
expect_reply=True, expect_reply=True,

View File

@ -4,7 +4,6 @@ from unittest.mock import AsyncMock, call, patch, sentinel
import pytest import pytest
import zigpy.profiles.zha as zha import zigpy.profiles.zha as zha
import zigpy.types
import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.general as general
import zigpy.zcl.clusters.lighting as lighting import zigpy.zcl.clusters.lighting as lighting
import zigpy.zcl.foundation as zcl_f 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.call_count == 1
assert cluster.request.await_count == 1 assert cluster.request.await_count == 1
assert cluster.request.call_args == call( 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) 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.call_count == 1
assert cluster.request.await_count == 1 assert cluster.request.await_count == 1
assert cluster.request.call_args == call( 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.call_count == 0
assert level_cluster.request.await_count == 0 assert level_cluster.request.await_count == 0
assert on_off_cluster.request.call_args == call( 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() on_off_cluster.request.reset_mock()
level_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.call_count == 1
assert level_cluster.request.await_count == 1 assert level_cluster.request.await_count == 1
assert on_off_cluster.request.call_args == call( 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( assert level_cluster.request.call_args == call(
False, False,
4, 4,
(zigpy.types.uint8_t, zigpy.types.uint16_t), level_cluster.commands_by_name["move_to_level_with_on_off"].schema,
254, 254,
100.0, 100.0,
expect_reply=True, expect_reply=True,
@ -419,7 +442,7 @@ async def async_test_level_on_off_from_hass(
assert level_cluster.request.call_args == call( assert level_cluster.request.call_args == call(
False, False,
4, 4,
(zigpy.types.uint8_t, zigpy.types.uint16_t), level_cluster.commands_by_name["move_to_level_with_on_off"].schema,
10, 10,
1, 1,
expect_reply=True, 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( assert cluster.request.call_args == call(
False, False,
64, 64,
(zigpy.types.uint8_t, zigpy.types.uint8_t), cluster.commands_by_name["trigger_effect"].schema,
FLASH_EFFECTS[flash], FLASH_EFFECTS[flash],
0, 0,
expect_reply=True, expect_reply=True,

View File

@ -307,7 +307,7 @@ async def async_test_device_temperature(hass, cluster, entity_id):
"metering_device_type": 0x00, "metering_device_type": 0x00,
"multiplier": 1, "multiplier": 1,
"status": 0x00, "status": 0x00,
"summa_formatting": 0b1_0111_010, "summation_formatting": 0b1_0111_010,
"unit_of_measure": 0x01, "unit_of_measure": 0x01,
}, },
{"instaneneous_demand"}, {"instaneneous_demand"},
@ -814,7 +814,7 @@ async def test_se_summation_uom(
"metering_device_type": 0x00, "metering_device_type": 0x00,
"multiplier": 1, "multiplier": 1,
"status": 0x00, "status": 0x00,
"summa_formatting": 0b1_0111_010, "summation_formatting": 0b1_0111_010,
"unit_of_measure": raw_uom, "unit_of_measure": raw_uom,
} }
await zha_device_joined(zigpy_device) 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 len(cluster.request.mock_calls) == 1
assert cluster.request.call_args == call( 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 # 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 len(cluster.request.mock_calls) == 1
assert cluster.request.call_args == call( 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 # 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 len(group_cluster_on_off.request.mock_calls) == 1
assert group_cluster_on_off.request.call_args == call( 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 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 len(group_cluster_on_off.request.mock_calls) == 1
assert group_cluster_on_off.request.call_args == call( 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 assert hass.states.get(entity_id).state == STATE_OFF