Skip to content

Commit

Permalink
Fix group entity availability handling (#164)
Browse files Browse the repository at this point in the history
* clean up update_available

* fix update debouncer and availability calc for group entities

* remove availabilty from group entity extensions as it is handled in the base now

* tests

* undo this change as it may cause unnecessary state changed events

* test the intermediate states

* clean up tests a bit
  • Loading branch information
dmulcahey authored Aug 12, 2024
1 parent 52a6a67 commit 5384ba5
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 16 deletions.
52 changes: 52 additions & 0 deletions tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,55 @@ def get_entity(
raise KeyError(
f"No {entity_type} entity found for platform {platform!r} on device {device}: {device.platform_entities}"
)


async def group_entity_availability_test(
zha_gateway: Gateway, device_1: Device, device_2: Device, entity: GroupEntity
):
"""Test group entity availability handling."""

assert entity.state["available"] is True

device_1.on_network = False
await asyncio.sleep(0.1)
await zha_gateway.async_block_till_done()
assert entity.state["available"] is True

device_2.on_network = False
await asyncio.sleep(0.1)
await zha_gateway.async_block_till_done()

assert entity.state["available"] is False

device_1.on_network = True
await asyncio.sleep(0.1)
await zha_gateway.async_block_till_done()
assert entity.state["available"] is True

device_2.on_network = True
await asyncio.sleep(0.1)
await zha_gateway.async_block_till_done()

assert entity.state["available"] is True

device_1.available = False
await asyncio.sleep(0.1)
await zha_gateway.async_block_till_done()
assert entity.state["available"] is True

device_2.available = False
await asyncio.sleep(0.1)
await zha_gateway.async_block_till_done()

assert entity.state["available"] is False

device_1.available = True
await asyncio.sleep(0.1)
await zha_gateway.async_block_till_done()
assert entity.state["available"] is True

device_2.available = True
await asyncio.sleep(0.1)
await zha_gateway.async_block_till_done()

assert entity.state["available"] is True
11 changes: 10 additions & 1 deletion tests/test_fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@
from zigpy.zcl.clusters import general, hvac
import zigpy.zcl.foundation as zcl_f

from tests.common import get_entity, get_group_entity, send_attributes_report
from tests.common import (
get_entity,
get_group_entity,
group_entity_availability_test,
send_attributes_report,
)
from tests.conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
from zha.application import Platform
from zha.application.gateway import Gateway
Expand Down Expand Up @@ -362,6 +367,10 @@ async def test_zha_group_fan_entity(
# test that group fan is now off
assert entity.state["is_on"] is False

await group_entity_availability_test(
zha_gateway, device_fan_1, device_fan_2, entity
)


@patch(
"zigpy.zcl.clusters.hvac.Fan.write_attributes",
Expand Down
5 changes: 5 additions & 0 deletions tests/test_light.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from tests.common import (
get_entity,
get_group_entity,
group_entity_availability_test,
send_attributes_report,
update_attribute_cache,
)
Expand Down Expand Up @@ -920,6 +921,10 @@ async def test_zha_group_light_entity(
await zha_gateway.async_block_till_done()
assert bool(entity.state["on"]) is True

await group_entity_availability_test(
zha_gateway, device_light_1, device_light_2, entity
)

# turn it off to test a new member add being tracked
await send_attributes_report(zha_gateway, dev1_cluster_on_off, {0: 0})
await zha_gateway.async_block_till_done()
Expand Down
5 changes: 5 additions & 0 deletions tests/test_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from tests.common import (
get_entity,
get_group_entity,
group_entity_availability_test,
send_attributes_report,
update_attribute_cache,
)
Expand Down Expand Up @@ -350,6 +351,10 @@ async def test_zha_group_switch_entity(
# test that group light is now back on
assert bool(entity.state["state"]) is True

await group_entity_availability_test(
zha_gateway, device_switch_1, device_switch_2, entity
)


class WindowDetectionFunctionQuirk(CustomDevice):
"""Quirk with window detection function attribute."""
Expand Down
17 changes: 16 additions & 1 deletion zha/application/platforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ def __init__(
_LOGGER,
cooldown=update_group_from_member_delay,
immediate=False,
function=self.async_update,
function=self.update,
)
self._group.register_group_entity(self)

Expand All @@ -442,6 +442,21 @@ def info_object(self) -> BaseEntityInfo:
group_id=self.group_id,
)

@property
def state(self) -> dict[str, Any]:
"""Return the arguments to use in the command."""
state = super().state
state["available"] = self.available
return state

@property
def available(self) -> bool:
"""Return true if all member entities are available."""
return any(
platform_entity.available
for platform_entity in self._group.get_platform_entities(self.PLATFORM)
)

@property
def group_id(self) -> int:
"""Return the group id."""
Expand Down
2 changes: 0 additions & 2 deletions zha/application/platforms/fan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,6 @@ def __init__(self, group: Group):
"""Initialize a fan group."""
self._fan_cluster_handler: ClusterHandler = group.endpoint[hvac.Fan.cluster_id]
super().__init__(group)
self._available: bool = False
self._percentage = None
self._preset_mode = None
if hasattr(self, "info_object"):
Expand Down Expand Up @@ -355,7 +354,6 @@ def update(self, _: Any = None) -> None:
"All platform entity states for group entity members: %s", all_states
)

self._available = any(entity.available for entity in platform_entities)
percentage_states: list[dict] = [
state for state in all_states if state.get(ATTR_PERCENTAGE)
]
Expand Down
11 changes: 0 additions & 11 deletions zha/application/platforms/light/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ def __init__(self, *args, **kwargs):
"""Initialize the light."""
self._device: Device = None
super().__init__(*args, **kwargs)
self._available: bool = False
self._min_mireds: int | None = 153
self._max_mireds: int | None = 500
self._hs_color: tuple[float, float] | None = None
Expand Down Expand Up @@ -1207,12 +1206,6 @@ def info_object(self) -> LightEntityInfo:
max_mireds=self.max_mireds,
)

# remove this when all ZHA platforms and base entities are updated
@property
def available(self) -> bool:
"""Return entity availability."""
return self._available

async def on_remove(self) -> None:
"""Cancel tasks this entity owns."""
await super().on_remove()
Expand Down Expand Up @@ -1261,10 +1254,6 @@ def update(self, _: Any = None) -> None:
self._off_with_transition = False
self._off_brightness = None

self._available = any(
platform_entity.device.available for platform_entity in platform_entities
)

self._brightness = reduce_attribute(on_states, ATTR_BRIGHTNESS)

self._xy_color = reduce_attribute(on_states, ATTR_XY_COLOR, reduce=mean_tuple)
Expand Down
1 change: 0 additions & 1 deletion zha/application/platforms/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,6 @@ def update(self, _: Any | None = None) -> None:
on_states = [state for state in all_states if state["state"]]

self._state = len(on_states) > 0
self._available = any(entity.available for entity in platform_entities)

self.maybe_emit_state_changed_event()

Expand Down

0 comments on commit 5384ba5

Please sign in to comment.