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
50 changes: 50 additions & 0 deletions tests/test_fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -838,3 +838,53 @@ async def test_fan_kof_update_entity(
assert entity.state.percentage == 25
assert entity.state.preset_mode is None
assert entity.percentage_step == 100 / 4


async def test_group_state_ignores_unavailable_members(zha_gateway: Gateway) -> None:
"""Test that an unavailable member does not determine the fan group state."""
device_fan_1 = await device_fan_1_mock(zha_gateway)
device_fan_2 = await device_fan_2_mock(zha_gateway)
members = [
GroupMemberReference(ieee=device_fan_1.ieee, endpoint_id=1),
GroupMemberReference(ieee=device_fan_2.ieee, endpoint_id=1),
]
zha_group: Group = await zha_gateway.async_create_zigpy_group("Test Group", members)
await zha_gateway.async_block_till_done()

entity: GroupEntity = get_group_entity(zha_group, platform=Platform.FAN)
device_1_entity = get_entity(device_fan_1, platform=Platform.FAN)

dev1_fan_cluster = device_fan_1.device.endpoints[1].fan
dev2_fan_cluster = device_fan_2.device.endpoints[1].fan

# first member on high, second on low: the first member's speed wins
await send_attributes_report(zha_gateway, dev1_fan_cluster, {0: 3})
await send_attributes_report(zha_gateway, dev2_fan_cluster, {0: 1})
# group member updates are debounced
await asyncio.sleep(1)
await zha_gateway.async_block_till_done()
assert entity.state.percentage == 100

# the first member drops off the network while it is still on high
device_fan_1.update_available(False)
await asyncio.sleep(1)
await zha_gateway.async_block_till_done()
assert device_1_entity.state.available is False
assert device_1_entity.state.percentage == 100

# the reachable member's speed is used instead of the stale one
assert entity.state.percentage == 33
assert entity.state.is_on is True

# turning the reachable member off turns the group off
await send_attributes_report(zha_gateway, dev2_fan_cluster, {0: 0})
await asyncio.sleep(1)
await zha_gateway.async_block_till_done()
assert entity.state.is_on is False

# once it is reachable again its state counts
device_fan_1.update_available(True)
await asyncio.sleep(1)
await zha_gateway.async_block_till_done()
assert entity.state.percentage == 100
assert entity.state.is_on is True
45 changes: 45 additions & 0 deletions tests/test_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -904,3 +904,48 @@ async def test_binary_output_cluster(zha_gateway: Gateway) -> None:
manufacturer=UNDEFINED,
)
]


async def test_group_state_ignores_unavailable_members(zha_gateway: Gateway) -> None:
"""Test that an unavailable member does not keep the group on."""
device_switch_1 = await device_switch_1_mock(zha_gateway)
device_switch_2 = await device_switch_2_mock(zha_gateway)
members = [
GroupMemberReference(ieee=device_switch_1.ieee, endpoint_id=1),
GroupMemberReference(ieee=device_switch_2.ieee, endpoint_id=1),
]
zha_group: Group = await zha_gateway.async_create_zigpy_group("Test Group", members)
await zha_gateway.async_block_till_done()

entity: GroupEntity = get_group_entity(zha_group, platform=Platform.SWITCH)
device_2_entity = get_entity(device_switch_2, platform=Platform.SWITCH)

dev1_cluster_on_off = device_switch_1.device.endpoints[1].on_off
dev2_cluster_on_off = device_switch_2.device.endpoints[1].on_off

# both members on, so the group is on
await send_attributes_report(zha_gateway, dev1_cluster_on_off, {0: 1})
await send_attributes_report(zha_gateway, dev2_cluster_on_off, {0: 1})
# group member updates are debounced
await asyncio.sleep(1)
await zha_gateway.async_block_till_done()
assert bool(entity.state.is_on) is True

# the second member drops off the network while it is still on
device_switch_2.update_available(False)
await asyncio.sleep(1)
await zha_gateway.async_block_till_done()
assert device_2_entity.state.available is False
assert bool(device_2_entity.state.is_on) is True

# turning the reachable member off turns the group off
await send_attributes_report(zha_gateway, dev1_cluster_on_off, {0: 0})
await asyncio.sleep(1)
await zha_gateway.async_block_till_done()
assert bool(entity.state.is_on) is False

# once it is reachable again its state counts, and the group is on
device_switch_2.update_available(True)
await asyncio.sleep(1)
await zha_gateway.async_block_till_done()
assert bool(entity.state.is_on) is True
6 changes: 4 additions & 2 deletions zha/application/platforms/fan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,8 +368,10 @@ def update(self, _: Any = None) -> None:
"All platform entity states for group entity members: %s", all_states
)

percentage_states = [state for state in all_states if state.percentage]
preset_mode_states = [state for state in all_states if state.preset_mode]
# Unavailable members keep their last state, ignore them
states = [state for state in all_states if state.available]
percentage_states = [state for state in states if state.percentage]
preset_mode_states = [state for state in states if state.preset_mode]

if percentage_states:
self._percentage = percentage_states[0].percentage
Expand Down
5 changes: 3 additions & 2 deletions zha/application/platforms/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,14 +376,15 @@ async def async_turn_off(self) -> None:
self.maybe_emit_state_changed_event()

def update(self, _: Any | None = None) -> None:
"""Query all members and determine the light group state."""
"""Query all members and determine the switch group state."""
self.debug("Updating switch group entity state")
platform_entities = self._group.get_platform_entities(self.PLATFORM)
all_states = [cast(SwitchState, entity.state) for entity in platform_entities]
self.debug(
"All platform entity states for group entity members: %s", all_states
)
on_states = [state for state in all_states if state.is_on]
# Unavailable members keep their last state, don't let them keep the group on
on_states = [state for state in all_states if state.is_on and state.available]

self._state = len(on_states) > 0

Expand Down
Loading