Add support for unavailable to cover groups (#74053)

This commit is contained in:
Erik Montnemery 2022-06-28 11:12:14 +02:00 committed by GitHub
parent af71c250d5
commit ae63cd8677
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 49 additions and 32 deletions

View File

@ -35,6 +35,8 @@ from homeassistant.const import (
STATE_CLOSING, STATE_CLOSING,
STATE_OPEN, STATE_OPEN,
STATE_OPENING, STATE_OPENING,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
) )
from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.core import Event, HomeAssistant, State, callback
from homeassistant.helpers import config_validation as cv, entity_registry as er 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): class CoverGroup(GroupEntity, CoverEntity):
"""Representation of a CoverGroup.""" """Representation of a CoverGroup."""
_attr_available: bool = False
_attr_is_closed: bool | None = None _attr_is_closed: bool | None = None
_attr_is_opening: bool | None = False _attr_is_opening: bool | None = False
_attr_is_closing: bool | None = False _attr_is_closing: bool | None = False
@ -267,29 +270,38 @@ class CoverGroup(GroupEntity, CoverEntity):
"""Update state and attributes.""" """Update state and attributes."""
self._attr_assumed_state = False 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_closed = True
self._attr_is_closing = False self._attr_is_closing = False
self._attr_is_opening = False self._attr_is_opening = False
has_valid_state = False
for entity_id in self._entities: for entity_id in self._entities:
if not (state := self.hass.states.get(entity_id)): if not (state := self.hass.states.get(entity_id)):
continue continue
if state.state == STATE_OPEN: if state.state == STATE_OPEN:
self._attr_is_closed = False self._attr_is_closed = False
has_valid_state = True
continue continue
if state.state == STATE_CLOSED: if state.state == STATE_CLOSED:
has_valid_state = True
continue continue
if state.state == STATE_CLOSING: if state.state == STATE_CLOSING:
self._attr_is_closing = True self._attr_is_closing = True
has_valid_state = True
continue continue
if state.state == STATE_OPENING: if state.state == STATE_OPENING:
self._attr_is_opening = True self._attr_is_opening = True
has_valid_state = True
continue 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 self._attr_is_closed = None
position_covers = self._covers[KEY_POSITION] position_covers = self._covers[KEY_POSITION]

View File

@ -109,32 +109,36 @@ async def test_state(hass, setup_comp):
Otherwise, the group state is closed. Otherwise, the group state is closed.
""" """
state = hass.states.get(COVER_GROUP) state = hass.states.get(COVER_GROUP)
# No entity has a valid state -> group state unknown # No entity has a valid state -> group state unavailable
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNAVAILABLE
assert state.attributes[ATTR_FRIENDLY_NAME] == DEFAULT_NAME 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] == [ assert state.attributes[ATTR_ENTITY_ID] == [
DEMO_COVER, DEMO_COVER,
DEMO_COVER_POS, DEMO_COVER_POS,
DEMO_COVER_TILT, DEMO_COVER_TILT,
DEMO_TILT, DEMO_TILT,
] ]
assert ATTR_ASSUMED_STATE not in state.attributes
assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 # The group state is unavailable if all group members are unavailable.
assert ATTR_CURRENT_POSITION not in state.attributes hass.states.async_set(DEMO_COVER, STATE_UNAVAILABLE, {})
assert ATTR_CURRENT_TILT_POSITION not in state.attributes 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. # 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_1 in (STATE_UNAVAILABLE, STATE_UNKNOWN):
for state_2 in (STATE_UNAVAILABLE, STATE_UNKNOWN): for state_2 in (STATE_UNAVAILABLE, STATE_UNKNOWN):
for state_3 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) state = hass.states.get(COVER_GROUP)
assert state.state == STATE_CLOSED 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)
hass.states.async_remove(DEMO_COVER_POS) hass.states.async_remove(DEMO_COVER_POS)
hass.states.async_remove(DEMO_COVER_TILT) hass.states.async_remove(DEMO_COVER_TILT)
hass.states.async_remove(DEMO_TILT) hass.states.async_remove(DEMO_TILT)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(COVER_GROUP) state = hass.states.get(COVER_GROUP)
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNAVAILABLE
@pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)]) @pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)])
async def test_attributes(hass, setup_comp): async def test_attributes(hass, setup_comp):
"""Test handling of state attributes.""" """Test handling of state attributes."""
state = hass.states.get(COVER_GROUP) 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_FRIENDLY_NAME] == DEFAULT_NAME
assert state.attributes[ATTR_ENTITY_ID] == [ assert ATTR_ENTITY_ID not in state.attributes
DEMO_COVER,
DEMO_COVER_POS,
DEMO_COVER_TILT,
DEMO_TILT,
]
assert ATTR_ASSUMED_STATE not in state.attributes assert ATTR_ASSUMED_STATE not in state.attributes
assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0
assert ATTR_CURRENT_POSITION not in state.attributes 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) state = hass.states.get(COVER_GROUP)
assert state.state == STATE_CLOSED 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 # Set entity as opening
hass.states.async_set(DEMO_COVER, STATE_OPENING, {}) hass.states.async_set(DEMO_COVER, STATE_OPENING, {})