diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index c2cd3c6e9d2..a867c92d956 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -35,6 +35,8 @@ from homeassistant.const import ( STATE_CLOSING, STATE_OPEN, STATE_OPENING, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er @@ -98,6 +100,7 @@ async def async_setup_entry( class CoverGroup(GroupEntity, CoverEntity): """Representation of a CoverGroup.""" + _attr_available: bool = False _attr_is_closed: bool | None = None _attr_is_opening: bool | None = False _attr_is_closing: bool | None = False @@ -267,29 +270,38 @@ class CoverGroup(GroupEntity, CoverEntity): """Update state and attributes.""" self._attr_assumed_state = False + states = [ + state.state + for entity_id in self._entities + if (state := self.hass.states.get(entity_id)) is not None + ] + + valid_state = any( + state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in states + ) + + # Set group as unavailable if all members are unavailable or missing + self._attr_available = any(state != STATE_UNAVAILABLE for state in states) + self._attr_is_closed = True self._attr_is_closing = False self._attr_is_opening = False - has_valid_state = False for entity_id in self._entities: if not (state := self.hass.states.get(entity_id)): continue if state.state == STATE_OPEN: self._attr_is_closed = False - has_valid_state = True continue if state.state == STATE_CLOSED: - has_valid_state = True continue if state.state == STATE_CLOSING: self._attr_is_closing = True - has_valid_state = True continue if state.state == STATE_OPENING: self._attr_is_opening = True - has_valid_state = True continue - if not has_valid_state: + if not valid_state: + # Set as unknown if all members are unknown or unavailable self._attr_is_closed = None position_covers = self._covers[KEY_POSITION] diff --git a/tests/components/group/test_cover.py b/tests/components/group/test_cover.py index 83c85a70b63..57c54c7c502 100644 --- a/tests/components/group/test_cover.py +++ b/tests/components/group/test_cover.py @@ -109,32 +109,36 @@ async def test_state(hass, setup_comp): Otherwise, the group state is closed. """ state = hass.states.get(COVER_GROUP) - # No entity has a valid state -> group state unknown - assert state.state == STATE_UNKNOWN + # No entity has a valid state -> group state unavailable + assert state.state == STATE_UNAVAILABLE assert state.attributes[ATTR_FRIENDLY_NAME] == DEFAULT_NAME + assert ATTR_ENTITY_ID not in state.attributes + assert ATTR_ASSUMED_STATE not in state.attributes + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert ATTR_CURRENT_POSITION not in state.attributes + assert ATTR_CURRENT_TILT_POSITION not in state.attributes + + # Test group members exposed as attribute + hass.states.async_set(DEMO_COVER, STATE_UNKNOWN, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) assert state.attributes[ATTR_ENTITY_ID] == [ DEMO_COVER, DEMO_COVER_POS, DEMO_COVER_TILT, DEMO_TILT, ] - assert ATTR_ASSUMED_STATE not in state.attributes - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 - assert ATTR_CURRENT_POSITION not in state.attributes - assert ATTR_CURRENT_TILT_POSITION not in state.attributes + + # The group state is unavailable if all group members are unavailable. + hass.states.async_set(DEMO_COVER, STATE_UNAVAILABLE, {}) + hass.states.async_set(DEMO_COVER_POS, STATE_UNAVAILABLE, {}) + hass.states.async_set(DEMO_COVER_TILT, STATE_UNAVAILABLE, {}) + hass.states.async_set(DEMO_TILT, STATE_UNAVAILABLE, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_UNAVAILABLE # The group state is unknown if all group members are unknown or unavailable. - for state_1 in (STATE_UNAVAILABLE, STATE_UNKNOWN): - for state_2 in (STATE_UNAVAILABLE, STATE_UNKNOWN): - for state_3 in (STATE_UNAVAILABLE, STATE_UNKNOWN): - hass.states.async_set(DEMO_COVER, state_1, {}) - hass.states.async_set(DEMO_COVER_POS, state_2, {}) - hass.states.async_set(DEMO_COVER_TILT, state_3, {}) - hass.states.async_set(DEMO_TILT, STATE_UNAVAILABLE, {}) - await hass.async_block_till_done() - state = hass.states.get(COVER_GROUP) - assert state.state == STATE_UNKNOWN - for state_1 in (STATE_UNAVAILABLE, STATE_UNKNOWN): for state_2 in (STATE_UNAVAILABLE, STATE_UNKNOWN): for state_3 in (STATE_UNAVAILABLE, STATE_UNKNOWN): @@ -233,28 +237,23 @@ async def test_state(hass, setup_comp): state = hass.states.get(COVER_GROUP) assert state.state == STATE_CLOSED - # All group members removed from the state machine -> unknown + # All group members removed from the state machine -> unavailable hass.states.async_remove(DEMO_COVER) hass.states.async_remove(DEMO_COVER_POS) hass.states.async_remove(DEMO_COVER_TILT) hass.states.async_remove(DEMO_TILT) await hass.async_block_till_done() state = hass.states.get(COVER_GROUP) - assert state.state == STATE_UNKNOWN + assert state.state == STATE_UNAVAILABLE @pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)]) async def test_attributes(hass, setup_comp): """Test handling of state attributes.""" state = hass.states.get(COVER_GROUP) - assert state.state == STATE_UNKNOWN + assert state.state == STATE_UNAVAILABLE assert state.attributes[ATTR_FRIENDLY_NAME] == DEFAULT_NAME - assert state.attributes[ATTR_ENTITY_ID] == [ - DEMO_COVER, - DEMO_COVER_POS, - DEMO_COVER_TILT, - DEMO_TILT, - ] + assert ATTR_ENTITY_ID not in state.attributes assert ATTR_ASSUMED_STATE not in state.attributes assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 assert ATTR_CURRENT_POSITION not in state.attributes @@ -266,6 +265,12 @@ async def test_attributes(hass, setup_comp): state = hass.states.get(COVER_GROUP) assert state.state == STATE_CLOSED + assert state.attributes[ATTR_ENTITY_ID] == [ + DEMO_COVER, + DEMO_COVER_POS, + DEMO_COVER_TILT, + DEMO_TILT, + ] # Set entity as opening hass.states.async_set(DEMO_COVER, STATE_OPENING, {})