Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 55 additions & 18 deletions homeassistant/components/zha/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
WARNING_DEVICE_STROBE_HIGH,
WARNING_DEVICE_STROBE_YES,
)
from .core.group import GroupMember
from .core.helpers import async_is_bindable_target, get_matched_clusters

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -209,7 +210,7 @@ async def websocket_get_devices(hass, connection, msg):
"""Get ZHA devices."""
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)

Expand All @@ -221,13 +222,35 @@ async def websocket_get_groupable_devices(hass, connection, msg):
"""Get ZHA devices that can be grouped."""
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]

devices = [
device.async_get_info()
for device in zha_gateway.devices.values()
if device.is_groupable or device.is_coordinator
]
devices = [device for device in zha_gateway.devices.values() if device.is_groupable]
groupable_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], devices)
connection.send_result(msg[ID], groupable_devices)


@websocket_api.require_admin
Expand All @@ -236,7 +259,7 @@ async def websocket_get_groupable_devices(hass, connection, msg):
async def websocket_get_groups(hass, connection, msg):
"""Get ZHA groups."""
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)


Expand All @@ -251,7 +274,7 @@ async def websocket_get_device(hass, connection, msg):
ieee = msg[ATTR_IEEE]
device = None
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:
connection.send_message(
websocket_api.error_message(
Expand All @@ -274,7 +297,7 @@ async def websocket_get_group(hass, connection, msg):
group = None

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:
connection.send_message(
websocket_api.error_message(
Expand All @@ -285,13 +308,27 @@ async def websocket_get_group(hass, connection, msg):
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.async_response
@websocket_api.websocket_command(
{
vol.Required(TYPE): "zha/group/add",
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):
Expand All @@ -300,7 +337,7 @@ async def websocket_add_group(hass, connection, msg):
group_name = msg[GROUP_NAME]
members = msg.get(ATTR_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
Expand All @@ -323,7 +360,7 @@ async def websocket_remove_groups(hass, connection, msg):
await asyncio.gather(*tasks)
else:
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)


Expand All @@ -333,7 +370,7 @@ async def websocket_remove_groups(hass, connection, msg):
{
vol.Required(TYPE): "zha/group/members/add",
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):
Expand All @@ -353,7 +390,7 @@ async def websocket_add_group_members(hass, connection, msg):
)
)
return
ret_group = zha_group.async_get_info()
ret_group = zha_group.group_info
connection.send_result(msg[ID], ret_group)


Expand All @@ -363,7 +400,7 @@ async def websocket_add_group_members(hass, connection, msg):
{
vol.Required(TYPE): "zha/group/members/remove",
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):
Expand All @@ -383,7 +420,7 @@ async def websocket_remove_group_members(hass, connection, msg):
)
)
return
ret_group = zha_group.async_get_info()
ret_group = zha_group.group_info
connection.send_result(msg[ID], ret_group)


Expand Down Expand Up @@ -608,7 +645,7 @@ async def websocket_get_bindable_devices(hass, connection, msg):
source_device = zha_gateway.get_device(source_ieee)

devices = [
device.async_get_info()
device.zha_device_info
for device in zha_gateway.devices.values()
if async_is_bindable_target(source_device, device)
]
Expand Down
61 changes: 51 additions & 10 deletions homeassistant/components/zha/core/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,13 +235,9 @@ def is_end_device(self):
@property
def is_groupable(self):
"""Return true if this device has a group cluster."""
if not self.available:
return False
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
return self.is_coordinator or (
self.available and self.async_get_groupable_endpoints()
)

@property
def skip_configuration(self):
Expand Down Expand Up @@ -411,8 +407,8 @@ def async_update_last_seen(self, last_seen):
if self._zigpy_device.last_seen is None and last_seen is not None:
self._zigpy_device.last_seen = last_seen

@callback
def async_get_info(self):
@property
def zha_device_info(self):
"""Get ZHA device information."""
device_info = {}
device_info.update(self.device_info)
Expand Down Expand Up @@ -442,6 +438,15 @@ def async_get_clusters(self):
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
def async_get_std_clusters(self):
"""Get ZHA and ZLL clusters for this device."""
Expand Down Expand Up @@ -557,7 +562,15 @@ async def issue_cluster_command(

async def async_add_to_group(self, group_id):
"""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)
Comment thread
Adminiuga marked this conversation as resolved.
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):
"""Remove this device from the provided zigbee group."""
Expand All @@ -571,6 +584,34 @@ async def async_remove_from_group(self, group_id):
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):
"""Directly bind this device to a group for the given clusters."""
await self._async_group_binding_operation(
Expand Down
14 changes: 3 additions & 11 deletions homeassistant/components/zha/core/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,21 +235,13 @@ def determine_entity_domains(
) -> List[str]:
"""Determine the entity domains for this group."""
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]
all_domain_occurrences = []
for device in group.members:
if device.is_coordinator:
for member in group.members:
if member.device.is_coordinator:
continue
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(
[
Expand Down
31 changes: 18 additions & 13 deletions homeassistant/components/zha/core/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,11 @@
ZHA_GW_RADIO_DESCRIPTION,
)
from .device import DeviceStatus, ZHADevice
from .group import ZHAGroup
from .group import GroupMember, ZHAGroup
from .patches import apply_application_controller_patch
from .registries import GROUP_ENTITY_DOMAINS, RADIO_TYPES
from .store import async_get_registry
from .typing import ZhaDeviceType, ZhaGroupType, ZigpyEndpointType, ZigpyGroupType
from .typing import ZhaGroupType, ZigpyEndpointType, ZigpyGroupType

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -308,7 +308,7 @@ def _send_group_gateway_message(
ZHA_GW_MSG,
{
ATTR_TYPE: gateway_message_type,
ZHA_GW_MSG_GROUP_INFO: zha_group.async_get_info(),
ZHA_GW_MSG_GROUP_INFO: zha_group.group_info,
},
)

Expand All @@ -327,7 +327,7 @@ def device_removed(self, device):
zha_device = self._devices.pop(device.ieee, None)
entity_refs = self._device_registry.pop(device.ieee, 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()
async_dispatcher_send(
self._hass, "{}_{}".format(SIGNAL_REMOVE, str(zha_device.ieee))
Expand Down Expand Up @@ -542,7 +542,7 @@ async def async_device_initialized(self, device: zha_typing.ZigpyDeviceType):
)
await self._async_device_joined(zha_device)

device_info = zha_device.async_get_info()
device_info = zha_device.zha_device_info

async_dispatcher_send(
self._hass,
Expand Down Expand Up @@ -571,11 +571,11 @@ async def _async_device_rejoined(self, zha_device):
zha_device.update_available(True)

async def async_create_zigpy_group(
self, name: str, members: List[ZhaDeviceType]
self, name: str, members: List[GroupMember]
) -> ZhaGroupType:
"""Create a new Zigpy Zigbee group."""
# we start with one to fill any gaps from a user removing existing groups
group_id = 1
# we start with two to fill any gaps from a user removing existing groups
group_id = 2
while group_id in self.groups:
group_id += 1

Expand All @@ -584,14 +584,19 @@ async def async_create_zigpy_group(
self.application_controller.groups.add_group(group_id, name)
if members is not None:
tasks = []
for ieee in members:
for member in members:
_LOGGER.debug(
"Adding member with IEEE: %s to group: %s:0x%04x",
ieee,
"Adding member with IEEE: %s and endpoint id: %s to group: %s:0x%04x",
member.ieee,
member.endpoint_id,
name,
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)
return self.groups.get(group_id)

Expand All @@ -604,7 +609,7 @@ async def async_remove_zigpy_group(self, group_id: int) -> None:
if group and group.members:
tasks = []
for member in group.members:
tasks.append(member.async_remove_from_group(group_id))
tasks.append(member.async_remove_from_group())
if tasks:
await asyncio.gather(*tasks)
self.application_controller.groups.pop(group_id)
Expand Down
Loading