mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Add support for unavailable to cover groups (#74053)
This commit is contained in:
parent
af71c250d5
commit
ae63cd8677
@ -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]
|
||||||
|
@ -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, {})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user