Group by endpoints and not by devices for ZHA Zigbee groups (#34583)

* start implementation
* handle members correctly
* fix group member info
* align groupable devices with group members
* handle group endpoint adding and removing
* update add group
* update group and group member
* update create group
* remove domain check
* update test
* remove temporary 2nd groupable device api
* update test
* rename validator - review comment
* fix test that was never running
* additional testing
* fix coordinator descriptors
* remove check that was done twice
* update test
* Use AsyncMock()
Co-authored-by: Alexei Chetroi <lexoid@gmail.com>
This commit is contained in:
David F. Mulcahey 2020-05-04 15:19:53 -04:00 committed by GitHub
parent e54e9279e3
commit 8279efc164
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 438 additions and 167 deletions

View file

@ -53,6 +53,7 @@ from .core.const import (
WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_HIGH,
WARNING_DEVICE_STROBE_YES, WARNING_DEVICE_STROBE_YES,
) )
from .core.group import GroupMember
from .core.helpers import async_is_bindable_target, get_matched_clusters from .core.helpers import async_is_bindable_target, get_matched_clusters
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -209,7 +210,7 @@ async def websocket_get_devices(hass, connection, msg):
"""Get ZHA devices.""" """Get ZHA devices."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
devices = [device.async_get_info() for device in zha_gateway.devices.values()] devices = [device.zha_device_info for device in zha_gateway.devices.values()]
connection.send_result(msg[ID], devices) connection.send_result(msg[ID], devices)
@ -221,13 +222,35 @@ async def websocket_get_groupable_devices(hass, connection, msg):
"""Get ZHA devices that can be grouped.""" """Get ZHA devices that can be grouped."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
devices = [ devices = [device for device in zha_gateway.devices.values() if device.is_groupable]
device.async_get_info() groupable_devices = []
for device in zha_gateway.devices.values()
if device.is_groupable or device.is_coordinator
]
connection.send_result(msg[ID], devices) for device in devices:
entity_refs = zha_gateway.device_registry.get(device.ieee)
for ep_id in device.async_get_groupable_endpoints():
groupable_devices.append(
{
"endpoint_id": ep_id,
"entities": [
{
"name": zha_gateway.ha_entity_registry.async_get(
entity_ref.reference_id
).name,
"original_name": zha_gateway.ha_entity_registry.async_get(
entity_ref.reference_id
).original_name,
}
for entity_ref in entity_refs
if list(entity_ref.cluster_channels.values())[
0
].cluster.endpoint.endpoint_id
== ep_id
],
"device": device.zha_device_info,
}
)
connection.send_result(msg[ID], groupable_devices)
@websocket_api.require_admin @websocket_api.require_admin
@ -236,7 +259,7 @@ async def websocket_get_groupable_devices(hass, connection, msg):
async def websocket_get_groups(hass, connection, msg): async def websocket_get_groups(hass, connection, msg):
"""Get ZHA groups.""" """Get ZHA groups."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
groups = [group.async_get_info() for group in zha_gateway.groups.values()] groups = [group.group_info for group in zha_gateway.groups.values()]
connection.send_result(msg[ID], groups) connection.send_result(msg[ID], groups)
@ -251,7 +274,7 @@ async def websocket_get_device(hass, connection, msg):
ieee = msg[ATTR_IEEE] ieee = msg[ATTR_IEEE]
device = None device = None
if ieee in zha_gateway.devices: if ieee in zha_gateway.devices:
device = zha_gateway.devices[ieee].async_get_info() device = zha_gateway.devices[ieee].zha_device_info
if not device: if not device:
connection.send_message( connection.send_message(
websocket_api.error_message( websocket_api.error_message(
@ -274,7 +297,7 @@ async def websocket_get_group(hass, connection, msg):
group = None group = None
if group_id in zha_gateway.groups: if group_id in zha_gateway.groups:
group = zha_gateway.groups.get(group_id).async_get_info() group = zha_gateway.groups.get(group_id).group_info
if not group: if not group:
connection.send_message( connection.send_message(
websocket_api.error_message( websocket_api.error_message(
@ -285,13 +308,27 @@ async def websocket_get_group(hass, connection, msg):
connection.send_result(msg[ID], group) connection.send_result(msg[ID], group)
def cv_group_member(value: Any) -> GroupMember:
"""Validate and transform a group member."""
if not isinstance(value, Mapping):
raise vol.Invalid("Not a group member")
try:
group_member = GroupMember(
ieee=EUI64.convert(value["ieee"]), endpoint_id=value["endpoint_id"]
)
except KeyError:
raise vol.Invalid("Not a group member")
return group_member
@websocket_api.require_admin @websocket_api.require_admin
@websocket_api.async_response @websocket_api.async_response
@websocket_api.websocket_command( @websocket_api.websocket_command(
{ {
vol.Required(TYPE): "zha/group/add", vol.Required(TYPE): "zha/group/add",
vol.Required(GROUP_NAME): cv.string, vol.Required(GROUP_NAME): cv.string,
vol.Optional(ATTR_MEMBERS): vol.All(cv.ensure_list, [EUI64.convert]), vol.Optional(ATTR_MEMBERS): vol.All(cv.ensure_list, [cv_group_member]),
} }
) )
async def websocket_add_group(hass, connection, msg): async def websocket_add_group(hass, connection, msg):
@ -300,7 +337,7 @@ async def websocket_add_group(hass, connection, msg):
group_name = msg[GROUP_NAME] group_name = msg[GROUP_NAME]
members = msg.get(ATTR_MEMBERS) members = msg.get(ATTR_MEMBERS)
group = await zha_gateway.async_create_zigpy_group(group_name, members) group = await zha_gateway.async_create_zigpy_group(group_name, members)
connection.send_result(msg[ID], group.async_get_info()) connection.send_result(msg[ID], group.group_info)
@websocket_api.require_admin @websocket_api.require_admin
@ -323,7 +360,7 @@ async def websocket_remove_groups(hass, connection, msg):
await asyncio.gather(*tasks) await asyncio.gather(*tasks)
else: else:
await zha_gateway.async_remove_zigpy_group(group_ids[0]) await zha_gateway.async_remove_zigpy_group(group_ids[0])
ret_groups = [group.async_get_info() for group in zha_gateway.groups.values()] ret_groups = [group.group_info for group in zha_gateway.groups.values()]
connection.send_result(msg[ID], ret_groups) connection.send_result(msg[ID], ret_groups)
@ -333,7 +370,7 @@ async def websocket_remove_groups(hass, connection, msg):
{ {
vol.Required(TYPE): "zha/group/members/add", vol.Required(TYPE): "zha/group/members/add",
vol.Required(GROUP_ID): cv.positive_int, vol.Required(GROUP_ID): cv.positive_int,
vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [EUI64.convert]), vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [cv_group_member]),
} }
) )
async def websocket_add_group_members(hass, connection, msg): async def websocket_add_group_members(hass, connection, msg):
@ -353,7 +390,7 @@ async def websocket_add_group_members(hass, connection, msg):
) )
) )
return return
ret_group = zha_group.async_get_info() ret_group = zha_group.group_info
connection.send_result(msg[ID], ret_group) connection.send_result(msg[ID], ret_group)
@ -363,7 +400,7 @@ async def websocket_add_group_members(hass, connection, msg):
{ {
vol.Required(TYPE): "zha/group/members/remove", vol.Required(TYPE): "zha/group/members/remove",
vol.Required(GROUP_ID): cv.positive_int, vol.Required(GROUP_ID): cv.positive_int,
vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [EUI64.convert]), vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [cv_group_member]),
} }
) )
async def websocket_remove_group_members(hass, connection, msg): async def websocket_remove_group_members(hass, connection, msg):
@ -383,7 +420,7 @@ async def websocket_remove_group_members(hass, connection, msg):
) )
) )
return return
ret_group = zha_group.async_get_info() ret_group = zha_group.group_info
connection.send_result(msg[ID], ret_group) connection.send_result(msg[ID], ret_group)
@ -608,7 +645,7 @@ async def websocket_get_bindable_devices(hass, connection, msg):
source_device = zha_gateway.get_device(source_ieee) source_device = zha_gateway.get_device(source_ieee)
devices = [ devices = [
device.async_get_info() device.zha_device_info
for device in zha_gateway.devices.values() for device in zha_gateway.devices.values()
if async_is_bindable_target(source_device, device) if async_is_bindable_target(source_device, device)
] ]

View file

@ -235,13 +235,9 @@ class ZHADevice(LogMixin):
@property @property
def is_groupable(self): def is_groupable(self):
"""Return true if this device has a group cluster.""" """Return true if this device has a group cluster."""
if not self.available: return self.is_coordinator or (
return False self.available and self.async_get_groupable_endpoints()
clusters = self.async_get_clusters() )
for cluster_map in clusters.values():
for clusters in cluster_map.values():
if Groups.cluster_id in clusters:
return True
@property @property
def skip_configuration(self): def skip_configuration(self):
@ -411,8 +407,8 @@ class ZHADevice(LogMixin):
if self._zigpy_device.last_seen is None and last_seen is not None: if self._zigpy_device.last_seen is None and last_seen is not None:
self._zigpy_device.last_seen = last_seen self._zigpy_device.last_seen = last_seen
@callback @property
def async_get_info(self): def zha_device_info(self):
"""Get ZHA device information.""" """Get ZHA device information."""
device_info = {} device_info = {}
device_info.update(self.device_info) device_info.update(self.device_info)
@ -442,6 +438,15 @@ class ZHADevice(LogMixin):
if ep_id != 0 if ep_id != 0
} }
@callback
def async_get_groupable_endpoints(self):
"""Get device endpoints that have a group 'in' cluster."""
return [
ep_id
for (ep_id, clusters) in self.async_get_clusters().items()
if Groups.cluster_id in clusters[CLUSTER_TYPE_IN]
]
@callback @callback
def async_get_std_clusters(self): def async_get_std_clusters(self):
"""Get ZHA and ZLL clusters for this device.""" """Get ZHA and ZLL clusters for this device."""
@ -557,7 +562,15 @@ class ZHADevice(LogMixin):
async def async_add_to_group(self, group_id): async def async_add_to_group(self, group_id):
"""Add this device to the provided zigbee group.""" """Add this device to the provided zigbee group."""
await self._zigpy_device.add_to_group(group_id) try:
await self._zigpy_device.add_to_group(group_id)
except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex:
self.debug(
"Failed to add device '%s' to group: 0x%04x ex: %s",
self._zigpy_device.ieee,
group_id,
str(ex),
)
async def async_remove_from_group(self, group_id): async def async_remove_from_group(self, group_id):
"""Remove this device from the provided zigbee group.""" """Remove this device from the provided zigbee group."""
@ -571,6 +584,34 @@ class ZHADevice(LogMixin):
str(ex), str(ex),
) )
async def async_add_endpoint_to_group(self, endpoint_id, group_id):
"""Add the device endpoint to the provided zigbee group."""
try:
await self._zigpy_device.endpoints[int(endpoint_id)].add_to_group(group_id)
except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex:
self.debug(
"Failed to add endpoint: %s for device: '%s' to group: 0x%04x ex: %s",
endpoint_id,
self._zigpy_device.ieee,
group_id,
str(ex),
)
async def async_remove_endpoint_from_group(self, endpoint_id, group_id):
"""Remove the device endpoint from the provided zigbee group."""
try:
await self._zigpy_device.endpoints[int(endpoint_id)].remove_from_group(
group_id
)
except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex:
self.debug(
"Failed to remove endpoint: %s for device '%s' from group: 0x%04x ex: %s",
endpoint_id,
self._zigpy_device.ieee,
group_id,
str(ex),
)
async def async_bind_to_group(self, group_id, cluster_bindings): async def async_bind_to_group(self, group_id, cluster_bindings):
"""Directly bind this device to a group for the given clusters.""" """Directly bind this device to a group for the given clusters."""
await self._async_group_binding_operation( await self._async_group_binding_operation(

View file

@ -235,21 +235,13 @@ class GroupProbe:
) -> List[str]: ) -> List[str]:
"""Determine the entity domains for this group.""" """Determine the entity domains for this group."""
entity_domains: List[str] = [] entity_domains: List[str] = []
if len(group.members) < 2:
_LOGGER.debug(
"Group: %s:0x%04x has less than 2 members so cannot default an entity domain",
group.name,
group.group_id,
)
return entity_domains
zha_gateway = hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] zha_gateway = hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY]
all_domain_occurrences = [] all_domain_occurrences = []
for device in group.members: for member in group.members:
if device.is_coordinator: if member.device.is_coordinator:
continue continue
entities = async_entries_for_device( entities = async_entries_for_device(
zha_gateway.ha_entity_registry, device.device_id zha_gateway.ha_entity_registry, member.device.device_id
) )
all_domain_occurrences.extend( all_domain_occurrences.extend(
[ [

View file

@ -78,11 +78,11 @@ from .const import (
ZHA_GW_RADIO_DESCRIPTION, ZHA_GW_RADIO_DESCRIPTION,
) )
from .device import DeviceStatus, ZHADevice from .device import DeviceStatus, ZHADevice
from .group import ZHAGroup from .group import GroupMember, ZHAGroup
from .patches import apply_application_controller_patch from .patches import apply_application_controller_patch
from .registries import GROUP_ENTITY_DOMAINS, RADIO_TYPES from .registries import GROUP_ENTITY_DOMAINS, RADIO_TYPES
from .store import async_get_registry from .store import async_get_registry
from .typing import ZhaDeviceType, ZhaGroupType, ZigpyEndpointType, ZigpyGroupType from .typing import ZhaGroupType, ZigpyEndpointType, ZigpyGroupType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -308,7 +308,7 @@ class ZHAGateway:
ZHA_GW_MSG, ZHA_GW_MSG,
{ {
ATTR_TYPE: gateway_message_type, ATTR_TYPE: gateway_message_type,
ZHA_GW_MSG_GROUP_INFO: zha_group.async_get_info(), ZHA_GW_MSG_GROUP_INFO: zha_group.group_info,
}, },
) )
@ -327,7 +327,7 @@ class ZHAGateway:
zha_device = self._devices.pop(device.ieee, None) zha_device = self._devices.pop(device.ieee, None)
entity_refs = self._device_registry.pop(device.ieee, None) entity_refs = self._device_registry.pop(device.ieee, None)
if zha_device is not None: if zha_device is not None:
device_info = zha_device.async_get_info() device_info = zha_device.zha_device_info
zha_device.async_cleanup_handles() zha_device.async_cleanup_handles()
async_dispatcher_send( async_dispatcher_send(
self._hass, "{}_{}".format(SIGNAL_REMOVE, str(zha_device.ieee)) self._hass, "{}_{}".format(SIGNAL_REMOVE, str(zha_device.ieee))
@ -542,7 +542,7 @@ class ZHAGateway:
) )
await self._async_device_joined(zha_device) await self._async_device_joined(zha_device)
device_info = zha_device.async_get_info() device_info = zha_device.zha_device_info
async_dispatcher_send( async_dispatcher_send(
self._hass, self._hass,
@ -571,11 +571,11 @@ class ZHAGateway:
zha_device.update_available(True) zha_device.update_available(True)
async def async_create_zigpy_group( async def async_create_zigpy_group(
self, name: str, members: List[ZhaDeviceType] self, name: str, members: List[GroupMember]
) -> ZhaGroupType: ) -> ZhaGroupType:
"""Create a new Zigpy Zigbee group.""" """Create a new Zigpy Zigbee group."""
# we start with one to fill any gaps from a user removing existing groups # we start with two to fill any gaps from a user removing existing groups
group_id = 1 group_id = 2
while group_id in self.groups: while group_id in self.groups:
group_id += 1 group_id += 1
@ -584,14 +584,19 @@ class ZHAGateway:
self.application_controller.groups.add_group(group_id, name) self.application_controller.groups.add_group(group_id, name)
if members is not None: if members is not None:
tasks = [] tasks = []
for ieee in members: for member in members:
_LOGGER.debug( _LOGGER.debug(
"Adding member with IEEE: %s to group: %s:0x%04x", "Adding member with IEEE: %s and endpoint id: %s to group: %s:0x%04x",
ieee, member.ieee,
member.endpoint_id,
name, name,
group_id, group_id,
) )
tasks.append(self.devices[ieee].async_add_to_group(group_id)) tasks.append(
self.devices[member.ieee].async_add_endpoint_to_group(
member.endpoint_id, group_id
)
)
await asyncio.gather(*tasks) await asyncio.gather(*tasks)
return self.groups.get(group_id) return self.groups.get(group_id)
@ -604,7 +609,7 @@ class ZHAGateway:
if group and group.members: if group and group.members:
tasks = [] tasks = []
for member in group.members: for member in group.members:
tasks.append(member.async_remove_from_group(group_id)) tasks.append(member.async_remove_from_group())
if tasks: if tasks:
await asyncio.gather(*tasks) await asyncio.gather(*tasks)
self.application_controller.groups.pop(group_id) self.application_controller.groups.pop(group_id)

View file

@ -1,19 +1,110 @@
"""Group for Zigbee Home Automation.""" """Group for Zigbee Home Automation."""
import asyncio import asyncio
import collections
import logging import logging
from typing import Any, Dict, List from typing import Any, Dict, List
from zigpy.types.named import EUI64 import zigpy.exceptions
from homeassistant.core import callback
from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.entity_registry import async_entries_for_device
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from .helpers import LogMixin from .helpers import LogMixin
from .typing import ZhaDeviceType, ZhaGatewayType, ZigpyEndpointType, ZigpyGroupType from .typing import (
ZhaDeviceType,
ZhaGatewayType,
ZhaGroupType,
ZigpyEndpointType,
ZigpyGroupType,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
GroupMember = collections.namedtuple("GroupMember", "ieee endpoint_id")
GroupEntityReference = collections.namedtuple(
"GroupEntityReference", "name original_name entity_id"
)
class ZHAGroupMember(LogMixin):
"""Composite object that represents a device endpoint in a Zigbee group."""
def __init__(
self, zha_group: ZhaGroupType, zha_device: ZhaDeviceType, endpoint_id: int
):
"""Initialize the group member."""
self._zha_group: ZhaGroupType = zha_group
self._zha_device: ZhaDeviceType = zha_device
self._endpoint_id: int = endpoint_id
@property
def group(self) -> ZhaGroupType:
"""Return the group this member belongs to."""
return self._zha_group
@property
def endpoint_id(self) -> int:
"""Return the endpoint id for this group member."""
return self._endpoint_id
@property
def endpoint(self) -> ZigpyEndpointType:
"""Return the endpoint for this group member."""
return self._zha_device.device.endpoints.get(self.endpoint_id)
@property
def device(self) -> ZhaDeviceType:
"""Return the zha device for this group member."""
return self._zha_device
@property
def member_info(self) -> Dict[str, Any]:
"""Get ZHA group info."""
member_info: Dict[str, Any] = {}
member_info["endpoint_id"] = self.endpoint_id
member_info["device"] = self.device.zha_device_info
member_info["entities"] = self.associated_entities
return member_info
@property
def associated_entities(self) -> List[GroupEntityReference]:
"""Return the list of entities that were derived from this endpoint."""
ha_entity_registry = self.device.gateway.ha_entity_registry
zha_device_registry = self.device.gateway.device_registry
return [
GroupEntityReference(
ha_entity_registry.async_get(entity_ref.reference_id).name,
ha_entity_registry.async_get(entity_ref.reference_id).original_name,
entity_ref.reference_id,
)._asdict()
for entity_ref in zha_device_registry.get(self.device.ieee)
if list(entity_ref.cluster_channels.values())[
0
].cluster.endpoint.endpoint_id
== self.endpoint_id
]
async def async_remove_from_group(self) -> None:
"""Remove the device endpoint from the provided zigbee group."""
try:
await self._zha_device.device.endpoints[
self._endpoint_id
].remove_from_group(self._zha_group.group_id)
except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex:
self.debug(
"Failed to remove endpoint: %s for device '%s' from group: 0x%04x ex: %s",
self._endpoint_id,
self._zha_device.ieee,
self._zha_group.group_id,
str(ex),
)
def log(self, level: int, msg: str, *args) -> 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)
class ZHAGroup(LogMixin): class ZHAGroup(LogMixin):
"""ZHA Zigbee group object.""" """ZHA Zigbee group object."""
@ -45,77 +136,79 @@ class ZHAGroup(LogMixin):
return self._zigpy_group.endpoint return self._zigpy_group.endpoint
@property @property
def members(self) -> List[ZhaDeviceType]: def members(self) -> List[ZHAGroupMember]:
"""Return the ZHA devices that are members of this group.""" """Return the ZHA devices that are members of this group."""
return [ return [
self._zha_gateway.devices.get(member_ieee[0]) ZHAGroupMember(
for member_ieee in self._zigpy_group.members.keys() self, self._zha_gateway.devices.get(member_ieee), endpoint_id
if member_ieee[0] in self._zha_gateway.devices )
for (member_ieee, endpoint_id) in self._zigpy_group.members.keys()
if member_ieee in self._zha_gateway.devices
] ]
async def async_add_members(self, member_ieee_addresses: List[EUI64]) -> None: async def async_add_members(self, members: List[GroupMember]) -> None:
"""Add members to this group.""" """Add members to this group."""
if len(member_ieee_addresses) > 1: if len(members) > 1:
tasks = [] tasks = []
for ieee in member_ieee_addresses: for member in members:
tasks.append( tasks.append(
self._zha_gateway.devices[ieee].async_add_to_group(self.group_id) self._zha_gateway.devices[member.ieee].async_add_endpoint_to_group(
) member.endpoint_id, self.group_id
await asyncio.gather(*tasks)
else:
await self._zha_gateway.devices[
member_ieee_addresses[0]
].async_add_to_group(self.group_id)
async def async_remove_members(self, member_ieee_addresses: List[EUI64]) -> None:
"""Remove members from this group."""
if len(member_ieee_addresses) > 1:
tasks = []
for ieee in member_ieee_addresses:
tasks.append(
self._zha_gateway.devices[ieee].async_remove_from_group(
self.group_id
) )
) )
await asyncio.gather(*tasks) await asyncio.gather(*tasks)
else: else:
await self._zha_gateway.devices[ await self._zha_gateway.devices[
member_ieee_addresses[0] members[0].ieee
].async_remove_from_group(self.group_id) ].async_add_endpoint_to_group(members[0].endpoint_id, self.group_id)
async def async_remove_members(self, members: List[GroupMember]) -> None:
"""Remove members from this group."""
if len(members) > 1:
tasks = []
for member in members:
tasks.append(
self._zha_gateway.devices[
member.ieee
].async_remove_endpoint_from_group(
member.endpoint_id, self.group_id
)
)
await asyncio.gather(*tasks)
else:
await self._zha_gateway.devices[
members[0].ieee
].async_remove_endpoint_from_group(members[0].endpoint_id, self.group_id)
@property @property
def member_entity_ids(self) -> List[str]: def member_entity_ids(self) -> List[str]:
"""Return the ZHA entity ids for all entities for the members of this group.""" """Return the ZHA entity ids for all entities for the members of this group."""
all_entity_ids: List[str] = [] all_entity_ids: List[str] = []
for device in self.members: for member in self.members:
entities = async_entries_for_device( entity_references = member.associated_entities
self._zha_gateway.ha_entity_registry, device.device_id for entity_reference in entity_references:
) all_entity_ids.append(entity_reference["entity_id"])
for entity in entities:
all_entity_ids.append(entity.entity_id)
return all_entity_ids return all_entity_ids
def get_domain_entity_ids(self, domain) -> List[str]: def get_domain_entity_ids(self, domain) -> List[str]:
"""Return entity ids from the entity domain for this group.""" """Return entity ids from the entity domain for this group."""
domain_entity_ids: List[str] = [] domain_entity_ids: List[str] = []
for device in self.members: for member in self.members:
entities = async_entries_for_device( entities = async_entries_for_device(
self._zha_gateway.ha_entity_registry, device.device_id self._zha_gateway.ha_entity_registry, member.device.device_id
) )
domain_entity_ids.extend( domain_entity_ids.extend(
[entity.entity_id for entity in entities if entity.domain == domain] [entity.entity_id for entity in entities if entity.domain == domain]
) )
return domain_entity_ids return domain_entity_ids
@callback @property
def async_get_info(self) -> Dict[str, Any]: def group_info(self) -> Dict[str, Any]:
"""Get ZHA group info.""" """Get ZHA group info."""
group_info: Dict[str, Any] = {} group_info: Dict[str, Any] = {}
group_info["group_id"] = self.group_id group_info["group_id"] = self.group_id
group_info["name"] = self.name group_info["name"] = self.name
group_info["members"] = [ group_info["members"] = [member.member_info for member in self.members]
zha_device.async_get_info() for zha_device in self.members
]
return group_info return group_info
def log(self, level: int, msg: str, *args): def log(self, level: int, msg: str, *args):

View file

@ -32,7 +32,7 @@ class FakeEndpoint:
self.model = model self.model = model
self.profile_id = zigpy.profiles.zha.PROFILE_ID self.profile_id = zigpy.profiles.zha.PROFILE_ID
self.device_type = None self.device_type = None
self.request = AsyncMock() self.request = AsyncMock(return_value=[0])
def add_input_cluster(self, cluster_id): def add_input_cluster(self, cluster_id):
"""Add an input cluster.""" """Add an input cluster."""
@ -60,6 +60,7 @@ class FakeEndpoint:
FakeEndpoint.add_to_group = zigpy_ep.add_to_group FakeEndpoint.add_to_group = zigpy_ep.add_to_group
FakeEndpoint.remove_from_group = zigpy_ep.remove_from_group
def patch_cluster(cluster): def patch_cluster(cluster):

View file

@ -244,21 +244,26 @@ async def test_list_groupable_devices(zha_client, device_groupable):
assert msg["id"] == 10 assert msg["id"] == 10
assert msg["type"] == const.TYPE_RESULT assert msg["type"] == const.TYPE_RESULT
devices = msg["result"] device_endpoints = msg["result"]
assert len(devices) == 1 assert len(device_endpoints) == 1
for device in devices: for endpoint in device_endpoints:
assert device[ATTR_IEEE] == "01:2d:6f:00:0a:90:69:e8" assert endpoint["device"][ATTR_IEEE] == "01:2d:6f:00:0a:90:69:e8"
assert device[ATTR_MANUFACTURER] is not None assert endpoint["device"][ATTR_MANUFACTURER] is not None
assert device[ATTR_MODEL] is not None assert endpoint["device"][ATTR_MODEL] is not None
assert device[ATTR_NAME] is not None assert endpoint["device"][ATTR_NAME] is not None
assert device[ATTR_QUIRK_APPLIED] is not None assert endpoint["device"][ATTR_QUIRK_APPLIED] is not None
assert device["entities"] is not None assert endpoint["device"]["entities"] is not None
assert endpoint["endpoint_id"] is not None
assert endpoint["entities"] is not None
for entity_reference in device["entities"]: for entity_reference in endpoint["device"]["entities"]:
assert entity_reference[ATTR_NAME] is not None assert entity_reference[ATTR_NAME] is not None
assert entity_reference["entity_id"] is not None assert entity_reference["entity_id"] is not None
for entity_reference in endpoint["entities"]:
assert entity_reference["original_name"] is not None
# Make sure there are no groupable devices when the device is unavailable # Make sure there are no groupable devices when the device is unavailable
# Make device unavailable # Make device unavailable
device_groupable.set_available(False) device_groupable.set_available(False)
@ -269,8 +274,8 @@ async def test_list_groupable_devices(zha_client, device_groupable):
assert msg["id"] == 11 assert msg["id"] == 11
assert msg["type"] == const.TYPE_RESULT assert msg["type"] == const.TYPE_RESULT
devices = msg["result"] device_endpoints = msg["result"]
assert len(devices) == 0 assert len(device_endpoints) == 0
async def test_add_group(zha_client): async def test_add_group(zha_client):

View file

@ -62,6 +62,7 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined):
}, },
ieee="00:15:8d:00:02:32:4f:32", ieee="00:15:8d:00:02:32:4f:32",
nwk=0x0000, nwk=0x0000,
node_descriptor=b"\xf8\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff",
) )
zha_device = await zha_device_joined(zigpy_device) zha_device = await zha_device_joined(zigpy_device)
zha_device.set_available(True) zha_device.set_available(True)

View file

@ -1,6 +1,8 @@
"""Test ZHA Gateway.""" """Test ZHA Gateway."""
import asyncio
import logging import logging
import time import time
from unittest.mock import patch
import pytest import pytest
import zigpy.profiles.zha as zha import zigpy.profiles.zha as zha
@ -8,6 +10,7 @@ import zigpy.zcl.clusters.general as general
import zigpy.zcl.clusters.lighting as lighting import zigpy.zcl.clusters.lighting as lighting
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.zha.core.group import GroupMember
from .common import async_enable_traffic, async_find_group_entity_id, get_zha_gateway from .common import async_enable_traffic, async_find_group_entity_id, get_zha_gateway
@ -52,6 +55,7 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined):
}, },
ieee="00:15:8d:00:02:32:4f:32", ieee="00:15:8d:00:02:32:4f:32",
nwk=0x0000, nwk=0x0000,
node_descriptor=b"\xf8\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff",
) )
zha_device = await zha_device_joined(zigpy_device) zha_device = await zha_device_joined(zigpy_device)
zha_device.set_available(True) zha_device.set_available(True)
@ -127,17 +131,16 @@ async def test_gateway_group_methods(hass, device_light_1, device_light_2, coord
device_light_1._zha_gateway = zha_gateway device_light_1._zha_gateway = zha_gateway
device_light_2._zha_gateway = zha_gateway device_light_2._zha_gateway = zha_gateway
member_ieee_addresses = [device_light_1.ieee, device_light_2.ieee] member_ieee_addresses = [device_light_1.ieee, device_light_2.ieee]
members = [GroupMember(device_light_1.ieee, 1), GroupMember(device_light_2.ieee, 1)]
# test creating a group with 2 members # test creating a group with 2 members
zha_group = await zha_gateway.async_create_zigpy_group( zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members)
"Test Group", member_ieee_addresses
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert zha_group is not None assert zha_group is not None
assert len(zha_group.members) == 2 assert len(zha_group.members) == 2
for member in zha_group.members: for member in zha_group.members:
assert member.ieee in member_ieee_addresses assert member.device.ieee in member_ieee_addresses
entity_id = async_find_group_entity_id(hass, LIGHT_DOMAIN, zha_group) entity_id = async_find_group_entity_id(hass, LIGHT_DOMAIN, zha_group)
assert hass.states.get(entity_id) is not None assert hass.states.get(entity_id) is not None
@ -157,18 +160,24 @@ async def test_gateway_group_methods(hass, device_light_1, device_light_2, coord
# test creating a group with 1 member # test creating a group with 1 member
zha_group = await zha_gateway.async_create_zigpy_group( zha_group = await zha_gateway.async_create_zigpy_group(
"Test Group", [device_light_1.ieee] "Test Group", [GroupMember(device_light_1.ieee, 1)]
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert zha_group is not None assert zha_group is not None
assert len(zha_group.members) == 1 assert len(zha_group.members) == 1
for member in zha_group.members: for member in zha_group.members:
assert member.ieee in [device_light_1.ieee] assert member.device.ieee in [device_light_1.ieee]
# the group entity should not have been cleaned up # the group entity should not have been cleaned up
assert entity_id not in hass.states.async_entity_ids(LIGHT_DOMAIN) assert entity_id not in hass.states.async_entity_ids(LIGHT_DOMAIN)
with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError):
await zha_group.members[0].async_remove_from_group()
assert len(zha_group.members) == 1
for member in zha_group.members:
assert member.device.ieee in [device_light_1.ieee]
async def test_updating_device_store(hass, zigpy_dev_basic, zha_dev_basic): async def test_updating_device_store(hass, zigpy_dev_basic, zha_dev_basic):
"""Test saving data after a delay.""" """Test saving data after a delay."""

View file

@ -9,6 +9,7 @@ import zigpy.zcl.clusters.lighting as lighting
import zigpy.zcl.foundation as zcl_f import zigpy.zcl.foundation as zcl_f
from homeassistant.components.light import DOMAIN, FLASH_LONG, FLASH_SHORT from homeassistant.components.light import DOMAIN, FLASH_LONG, FLASH_SHORT
from homeassistant.components.zha.core.group import GroupMember
from homeassistant.components.zha.light import FLASH_EFFECTS from homeassistant.components.zha.light import FLASH_EFFECTS
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -28,8 +29,8 @@ from tests.common import async_fire_time_changed
ON = 1 ON = 1
OFF = 0 OFF = 0
IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8"
IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e9"
IEEE_GROUPABLE_DEVICE3 = "03:2d:6f:00:0a:90:69:e8" IEEE_GROUPABLE_DEVICE3 = "03:2d:6f:00:0a:90:69:e7"
LIGHT_ON_OFF = { LIGHT_ON_OFF = {
1: { 1: {
@ -77,13 +78,14 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined):
zigpy_device = zigpy_device_mock( zigpy_device = zigpy_device_mock(
{ {
1: { 1: {
"in_clusters": [], "in_clusters": [general.Groups.cluster_id],
"out_clusters": [], "out_clusters": [],
"device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT, "device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT,
} }
}, },
ieee="00:15:8d:00:02:32:4f:32", ieee="00:15:8d:00:02:32:4f:32",
nwk=0x0000, nwk=0x0000,
node_descriptor=b"\xf8\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff",
) )
zha_device = await zha_device_joined(zigpy_device) zha_device = await zha_device_joined(zigpy_device)
zha_device.set_available(True) zha_device.set_available(True)
@ -109,6 +111,7 @@ async def device_light_1(hass, zigpy_device_mock, zha_device_joined):
} }
}, },
ieee=IEEE_GROUPABLE_DEVICE, ieee=IEEE_GROUPABLE_DEVICE,
nwk=0xB79D,
) )
zha_device = await zha_device_joined(zigpy_device) zha_device = await zha_device_joined(zigpy_device)
zha_device.set_available(True) zha_device.set_available(True)
@ -134,6 +137,7 @@ async def device_light_2(hass, zigpy_device_mock, zha_device_joined):
} }
}, },
ieee=IEEE_GROUPABLE_DEVICE2, ieee=IEEE_GROUPABLE_DEVICE2,
nwk=0xC79E,
) )
zha_device = await zha_device_joined(zigpy_device) zha_device = await zha_device_joined(zigpy_device)
zha_device.set_available(True) zha_device.set_available(True)
@ -159,6 +163,7 @@ async def device_light_3(hass, zigpy_device_mock, zha_device_joined):
} }
}, },
ieee=IEEE_GROUPABLE_DEVICE3, ieee=IEEE_GROUPABLE_DEVICE3,
nwk=0xB89F,
) )
zha_device = await zha_device_joined(zigpy_device) zha_device = await zha_device_joined(zigpy_device)
zha_device.set_available(True) zha_device.set_available(True)
@ -289,10 +294,12 @@ async def async_test_on_off_from_light(hass, cluster, entity_id):
"""Test on off functionality from the light.""" """Test on off functionality from the light."""
# turn on at light # turn on at light
await send_attributes_report(hass, cluster, {1: 0, 0: 1, 2: 3}) await send_attributes_report(hass, cluster, {1: 0, 0: 1, 2: 3})
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_ON assert hass.states.get(entity_id).state == STATE_ON
# turn off at light # turn off at light
await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 3}) await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 3})
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF assert hass.states.get(entity_id).state == STATE_OFF
@ -300,6 +307,7 @@ async def async_test_on_from_light(hass, cluster, entity_id):
"""Test on off functionality from the light.""" """Test on off functionality from the light."""
# turn on at light # turn on at light
await send_attributes_report(hass, cluster, {1: -1, 0: 1, 2: 2}) await send_attributes_report(hass, cluster, {1: -1, 0: 1, 2: 2})
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_ON assert hass.states.get(entity_id).state == STATE_ON
@ -410,6 +418,7 @@ async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected
await send_attributes_report( await send_attributes_report(
hass, cluster, {1: level + 10, 0: level, 2: level - 10 or 22} hass, cluster, {1: level + 10, 0: level, 2: level - 10 or 22}
) )
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == expected_state assert hass.states.get(entity_id).state == expected_state
# hass uses None for brightness of 0 in state attributes # hass uses None for brightness of 0 in state attributes
if level == 0: if level == 0:
@ -438,7 +447,23 @@ async def async_test_flash_from_hass(hass, cluster, entity_id, flash):
) )
async def async_test_zha_group_light_entity( @patch(
"zigpy.zcl.clusters.lighting.Color.request",
new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]),
)
@patch(
"zigpy.zcl.clusters.general.Identify.request",
new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]),
)
@patch(
"zigpy.zcl.clusters.general.LevelControl.request",
new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]),
)
@patch(
"zigpy.zcl.clusters.general.OnOff.request",
new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]),
)
async def test_zha_group_light_entity(
hass, device_light_1, device_light_2, device_light_3, coordinator hass, device_light_1, device_light_2, device_light_3, coordinator
): ):
"""Test the light entity for a ZHA group.""" """Test the light entity for a ZHA group."""
@ -449,119 +474,180 @@ async def async_test_zha_group_light_entity(
device_light_1._zha_gateway = zha_gateway device_light_1._zha_gateway = zha_gateway
device_light_2._zha_gateway = zha_gateway device_light_2._zha_gateway = zha_gateway
member_ieee_addresses = [device_light_1.ieee, device_light_2.ieee] member_ieee_addresses = [device_light_1.ieee, device_light_2.ieee]
members = [GroupMember(device_light_1.ieee, 1), GroupMember(device_light_2.ieee, 1)]
assert coordinator.is_coordinator
# test creating a group with 2 members # test creating a group with 2 members
zha_group = await zha_gateway.async_create_zigpy_group( zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members)
"Test Group", member_ieee_addresses
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert zha_group is not None assert zha_group is not None
assert len(zha_group.members) == 2 assert len(zha_group.members) == 2
for member in zha_group.members: for member in zha_group.members:
assert member.ieee in member_ieee_addresses assert member.device.ieee in member_ieee_addresses
assert member.group == zha_group
assert member.endpoint is not None
entity_id = async_find_group_entity_id(hass, DOMAIN, zha_group) device_1_entity_id = await find_entity_id(DOMAIN, device_light_1, hass)
assert hass.states.get(entity_id) is not None device_2_entity_id = await find_entity_id(DOMAIN, device_light_2, hass)
device_3_entity_id = await find_entity_id(DOMAIN, device_light_3, hass)
assert (
device_1_entity_id != device_2_entity_id
and device_1_entity_id != device_3_entity_id
)
assert device_2_entity_id != device_3_entity_id
group_entity_id = async_find_group_entity_id(hass, DOMAIN, zha_group)
assert hass.states.get(group_entity_id) is not None
assert device_1_entity_id in zha_group.member_entity_ids
assert device_2_entity_id in zha_group.member_entity_ids
assert device_3_entity_id not in zha_group.member_entity_ids
group_cluster_on_off = zha_group.endpoint[general.OnOff.cluster_id] group_cluster_on_off = zha_group.endpoint[general.OnOff.cluster_id]
group_cluster_level = zha_group.endpoint[general.LevelControl.cluster_id] group_cluster_level = zha_group.endpoint[general.LevelControl.cluster_id]
group_cluster_identify = zha_group.endpoint[general.Identify.cluster_id] group_cluster_identify = zha_group.endpoint[general.Identify.cluster_id]
dev1_cluster_on_off = device_light_1.endpoints[1].on_off dev1_cluster_on_off = device_light_1.device.endpoints[1].on_off
dev2_cluster_on_off = device_light_2.endpoints[1].on_off dev2_cluster_on_off = device_light_2.device.endpoints[1].on_off
dev3_cluster_on_off = device_light_3.endpoints[1].on_off dev3_cluster_on_off = device_light_3.device.endpoints[1].on_off
dev1_cluster_level = device_light_1.device.endpoints[1].level
# test that the lights were created and that they are unavailable # test that the lights were created and that they are unavailable
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE assert hass.states.get(group_entity_id).state == STATE_UNAVAILABLE
# allow traffic to flow through the gateway and device # allow traffic to flow through the gateway and device
await async_enable_traffic(hass, zha_group.members) await async_enable_traffic(hass, [device_light_1, device_light_2, device_light_3])
await hass.async_block_till_done()
# test that the lights were created and are off # test that the lights were created and are off
assert hass.states.get(entity_id).state == STATE_OFF assert hass.states.get(group_entity_id).state == STATE_OFF
# test turning the lights on and off from the light
await async_test_on_off_from_light(hass, group_cluster_on_off, entity_id)
# test turning the lights on and off from the HA # test turning the lights on and off from the HA
await async_test_on_off_from_hass(hass, group_cluster_on_off, entity_id) await async_test_on_off_from_hass(hass, group_cluster_on_off, group_entity_id)
# test short flashing the lights from the HA # test short flashing the lights from the HA
await async_test_flash_from_hass( await async_test_flash_from_hass(
hass, group_cluster_identify, entity_id, FLASH_SHORT hass, group_cluster_identify, group_entity_id, FLASH_SHORT
) )
# test turning the lights on and off from the light
await async_test_on_off_from_light(hass, dev1_cluster_on_off, group_entity_id)
# test turning the lights on and off from the HA # test turning the lights on and off from the HA
await async_test_level_on_off_from_hass( await async_test_level_on_off_from_hass(
hass, group_cluster_on_off, group_cluster_level, entity_id hass, group_cluster_on_off, group_cluster_level, group_entity_id
) )
# test getting a brightness change from the network # test getting a brightness change from the network
await async_test_on_from_light(hass, group_cluster_on_off, entity_id) await async_test_on_from_light(hass, dev1_cluster_on_off, group_entity_id)
await async_test_dimmer_from_light( await async_test_dimmer_from_light(
hass, group_cluster_level, entity_id, 150, STATE_ON hass, dev1_cluster_level, group_entity_id, 150, STATE_ON
) )
# test long flashing the lights from the HA # test long flashing the lights from the HA
await async_test_flash_from_hass( await async_test_flash_from_hass(
hass, group_cluster_identify, entity_id, FLASH_LONG hass, group_cluster_identify, group_entity_id, FLASH_LONG
) )
assert len(zha_group.members) == 2
# test some of the group logic to make sure we key off states correctly # test some of the group logic to make sure we key off states correctly
await dev1_cluster_on_off.on() await send_attributes_report(hass, dev1_cluster_on_off, {0: 1})
await dev2_cluster_on_off.on() await send_attributes_report(hass, dev2_cluster_on_off, {0: 1})
await hass.async_block_till_done()
# test that group light is on # test that group light is on
assert hass.states.get(entity_id).state == STATE_ON assert hass.states.get(device_1_entity_id).state == STATE_ON
assert hass.states.get(device_2_entity_id).state == STATE_ON
assert hass.states.get(group_entity_id).state == STATE_ON
await dev1_cluster_on_off.off() await send_attributes_report(hass, dev1_cluster_on_off, {0: 0})
await hass.async_block_till_done()
# test that group light is still on # test that group light is still on
assert hass.states.get(entity_id).state == STATE_ON assert hass.states.get(device_1_entity_id).state == STATE_OFF
assert hass.states.get(device_2_entity_id).state == STATE_ON
assert hass.states.get(group_entity_id).state == STATE_ON
await dev2_cluster_on_off.off() await send_attributes_report(hass, dev2_cluster_on_off, {0: 0})
await hass.async_block_till_done()
# test that group light is now off # test that group light is now off
assert hass.states.get(entity_id).state == STATE_OFF assert hass.states.get(device_1_entity_id).state == STATE_OFF
assert hass.states.get(device_2_entity_id).state == STATE_OFF
assert hass.states.get(group_entity_id).state == STATE_OFF
await dev1_cluster_on_off.on() await send_attributes_report(hass, dev1_cluster_on_off, {0: 1})
await hass.async_block_till_done()
# test that group light is now back on # test that group light is now back on
assert hass.states.get(entity_id).state == STATE_ON assert hass.states.get(device_1_entity_id).state == STATE_ON
assert hass.states.get(device_2_entity_id).state == STATE_OFF
assert hass.states.get(group_entity_id).state == STATE_ON
# test that group light is now off # turn it off to test a new member add being tracked
await group_cluster_on_off.off() await send_attributes_report(hass, dev1_cluster_on_off, {0: 0})
assert hass.states.get(entity_id).state == STATE_OFF await hass.async_block_till_done()
assert hass.states.get(device_1_entity_id).state == STATE_OFF
assert hass.states.get(device_2_entity_id).state == STATE_OFF
assert hass.states.get(group_entity_id).state == STATE_OFF
# add a new member and test that his state is also tracked # add a new member and test that his state is also tracked
await zha_group.async_add_members([device_light_3.ieee]) await zha_group.async_add_members([GroupMember(device_light_3.ieee, 1)])
await dev3_cluster_on_off.on() await send_attributes_report(hass, dev3_cluster_on_off, {0: 1})
assert hass.states.get(entity_id).state == STATE_ON await hass.async_block_till_done()
assert device_3_entity_id in zha_group.member_entity_ids
assert len(zha_group.members) == 3
assert hass.states.get(device_1_entity_id).state == STATE_OFF
assert hass.states.get(device_2_entity_id).state == STATE_OFF
assert hass.states.get(device_3_entity_id).state == STATE_ON
assert hass.states.get(group_entity_id).state == STATE_ON
# make the group have only 1 member and now there should be no entity # make the group have only 1 member and now there should be no entity
await zha_group.async_remove_members([device_light_2.ieee, device_light_3.ieee]) await zha_group.async_remove_members(
[GroupMember(device_light_2.ieee, 1), GroupMember(device_light_3.ieee, 1)]
)
assert len(zha_group.members) == 1 assert len(zha_group.members) == 1
assert hass.states.get(entity_id).state is None assert hass.states.get(group_entity_id) is None
assert device_2_entity_id not in zha_group.member_entity_ids
assert device_3_entity_id not in zha_group.member_entity_ids
# make sure the entity registry entry is still there # make sure the entity registry entry is still there
assert zha_gateway.ha_entity_registry.async_get(entity_id) is not None assert zha_gateway.ha_entity_registry.async_get(group_entity_id) is not None
# add a member back and ensure that the group entity was created again # add a member back and ensure that the group entity was created again
await zha_group.async_add_members([device_light_3.ieee]) await zha_group.async_add_members([GroupMember(device_light_3.ieee, 1)])
await dev3_cluster_on_off.on() await send_attributes_report(hass, dev3_cluster_on_off, {0: 1})
assert hass.states.get(entity_id).state == STATE_ON await hass.async_block_till_done()
assert len(zha_group.members) == 2
assert hass.states.get(group_entity_id).state == STATE_ON
# add a 3rd member and ensure we still have an entity and we track the new one # add a 3rd member and ensure we still have an entity and we track the new one
await dev1_cluster_on_off.off() await send_attributes_report(hass, dev1_cluster_on_off, {0: 0})
await dev3_cluster_on_off.off() await send_attributes_report(hass, dev3_cluster_on_off, {0: 0})
assert hass.states.get(entity_id).state == STATE_OFF await hass.async_block_till_done()
assert hass.states.get(group_entity_id).state == STATE_OFF
# this will test that _reprobe_group is used correctly # this will test that _reprobe_group is used correctly
await zha_group.async_add_members([device_light_2.ieee]) await zha_group.async_add_members(
await dev2_cluster_on_off.on() [GroupMember(device_light_2.ieee, 1), GroupMember(coordinator.ieee, 1)]
assert hass.states.get(entity_id).state == STATE_ON )
await send_attributes_report(hass, dev2_cluster_on_off, {0: 1})
await hass.async_block_till_done()
assert len(zha_group.members) == 4
assert hass.states.get(group_entity_id).state == STATE_ON
await zha_group.async_remove_members([GroupMember(coordinator.ieee, 1)])
await hass.async_block_till_done()
assert hass.states.get(group_entity_id).state == STATE_ON
assert len(zha_group.members) == 3
# remove the group and ensure that there is no entity and that the entity registry is cleaned up # remove the group and ensure that there is no entity and that the entity registry is cleaned up
assert zha_gateway.ha_entity_registry.async_get(entity_id) is not None assert zha_gateway.ha_entity_registry.async_get(group_entity_id) is not None
await zha_gateway.async_remove_zigpy_group(zha_group.group_id) await zha_gateway.async_remove_zigpy_group(zha_group.group_id)
assert hass.states.get(entity_id).state is None assert hass.states.get(group_entity_id) is None
assert zha_gateway.ha_entity_registry.async_get(entity_id) is None assert zha_gateway.ha_entity_registry.async_get(group_entity_id) is None

View file

@ -53,6 +53,7 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined):
}, },
ieee="00:15:8d:00:02:32:4f:32", ieee="00:15:8d:00:02:32:4f:32",
nwk=0x0000, nwk=0x0000,
node_descriptor=b"\xf8\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff",
) )
zha_device = await zha_device_joined(zigpy_device) zha_device = await zha_device_joined(zigpy_device)
zha_device.set_available(True) zha_device.set_available(True)