diff --git a/tests/common.py b/tests/common.py index 11a944af..a729ead7 100644 --- a/tests/common.py +++ b/tests/common.py @@ -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 diff --git a/tests/test_fan.py b/tests/test_fan.py index 97d5e036..88526080 100644 --- a/tests/test_fan.py +++ b/tests/test_fan.py @@ -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 @@ -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", diff --git a/tests/test_light.py b/tests/test_light.py index a4a7a78c..8a64c592 100644 --- a/tests/test_light.py +++ b/tests/test_light.py @@ -19,6 +19,7 @@ from tests.common import ( get_entity, get_group_entity, + group_entity_availability_test, send_attributes_report, update_attribute_cache, ) @@ -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() diff --git a/tests/test_switch.py b/tests/test_switch.py index 56f7a546..a5fdbeac 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -26,6 +26,7 @@ from tests.common import ( get_entity, get_group_entity, + group_entity_availability_test, send_attributes_report, update_attribute_cache, ) @@ -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.""" diff --git a/zha/application/platforms/__init__.py b/zha/application/platforms/__init__.py index f41c182f..05ff8d26 100644 --- a/zha/application/platforms/__init__.py +++ b/zha/application/platforms/__init__.py @@ -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) @@ -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.""" diff --git a/zha/application/platforms/fan/__init__.py b/zha/application/platforms/fan/__init__.py index 1a18d795..86e0b533 100644 --- a/zha/application/platforms/fan/__init__.py +++ b/zha/application/platforms/fan/__init__.py @@ -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"): @@ -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) ] diff --git a/zha/application/platforms/light/__init__.py b/zha/application/platforms/light/__init__.py index 92fdb4b4..87c6034e 100644 --- a/zha/application/platforms/light/__init__.py +++ b/zha/application/platforms/light/__init__.py @@ -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 @@ -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() @@ -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) diff --git a/zha/application/platforms/switch.py b/zha/application/platforms/switch.py index ac5c1c93..dc0c4727 100644 --- a/zha/application/platforms/switch.py +++ b/zha/application/platforms/switch.py @@ -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()