Add support for unavailable and unknown to fan groups (#74054)
This commit is contained in:
parent
28c1a5c09f
commit
7d709c074d
2 changed files with 66 additions and 44 deletions
|
@ -32,6 +32,8 @@ from homeassistant.const import (
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_UNIQUE_ID,
|
CONF_UNIQUE_ID,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
|
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 FanGroup(GroupEntity, FanEntity):
|
class FanGroup(GroupEntity, FanEntity):
|
||||||
"""Representation of a FanGroup."""
|
"""Representation of a FanGroup."""
|
||||||
|
|
||||||
|
_attr_available: bool = False
|
||||||
_attr_assumed_state: bool = True
|
_attr_assumed_state: bool = True
|
||||||
|
|
||||||
def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None:
|
def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None:
|
||||||
|
@ -109,7 +112,7 @@ class FanGroup(GroupEntity, FanEntity):
|
||||||
self._direction = None
|
self._direction = None
|
||||||
self._supported_features = 0
|
self._supported_features = 0
|
||||||
self._speed_count = 100
|
self._speed_count = 100
|
||||||
self._is_on = False
|
self._is_on: bool | None = False
|
||||||
self._attr_name = name
|
self._attr_name = name
|
||||||
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entities}
|
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entities}
|
||||||
self._attr_unique_id = unique_id
|
self._attr_unique_id = unique_id
|
||||||
|
@ -125,7 +128,7 @@ class FanGroup(GroupEntity, FanEntity):
|
||||||
return self._speed_count
|
return self._speed_count
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool | None:
|
||||||
"""Return true if the entity is on."""
|
"""Return true if the entity is on."""
|
||||||
return self._is_on
|
return self._is_on
|
||||||
|
|
||||||
|
@ -270,11 +273,25 @@ class FanGroup(GroupEntity, FanEntity):
|
||||||
"""Update state and attributes."""
|
"""Update state and attributes."""
|
||||||
self._attr_assumed_state = False
|
self._attr_assumed_state = False
|
||||||
|
|
||||||
on_states: list[State] = list(
|
states = [
|
||||||
filter(None, [self.hass.states.get(x) for x in self._entities])
|
state
|
||||||
|
for entity_id in self._entities
|
||||||
|
if (state := self.hass.states.get(entity_id)) is not None
|
||||||
|
]
|
||||||
|
self._attr_assumed_state |= not states_equal(states)
|
||||||
|
|
||||||
|
# Set group as unavailable if all members are unavailable or missing
|
||||||
|
self._attr_available = any(state.state != STATE_UNAVAILABLE for state in states)
|
||||||
|
|
||||||
|
valid_state = any(
|
||||||
|
state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in states
|
||||||
)
|
)
|
||||||
self._is_on = any(state.state == STATE_ON for state in on_states)
|
if not valid_state:
|
||||||
self._attr_assumed_state |= not states_equal(on_states)
|
# Set as unknown if all members are unknown or unavailable
|
||||||
|
self._is_on = None
|
||||||
|
else:
|
||||||
|
# Set as ON if any member is ON
|
||||||
|
self._is_on = any(state.state == STATE_ON for state in states)
|
||||||
|
|
||||||
percentage_states = self._async_states_by_support_flag(
|
percentage_states = self._async_states_by_support_flag(
|
||||||
FanEntityFeature.SET_SPEED
|
FanEntityFeature.SET_SPEED
|
||||||
|
@ -306,5 +323,5 @@ class FanGroup(GroupEntity, FanEntity):
|
||||||
ior, [feature for feature in SUPPORTED_FLAGS if self._fans[feature]], 0
|
ior, [feature for feature in SUPPORTED_FLAGS if self._fans[feature]], 0
|
||||||
)
|
)
|
||||||
self._attr_assumed_state |= any(
|
self._attr_assumed_state |= any(
|
||||||
state.attributes.get(ATTR_ASSUMED_STATE) for state in on_states
|
state.attributes.get(ATTR_ASSUMED_STATE) for state in states
|
||||||
)
|
)
|
||||||
|
|
|
@ -119,15 +119,45 @@ async def test_state(hass, setup_comp):
|
||||||
Otherwise, the group state is off.
|
Otherwise, the group state is off.
|
||||||
"""
|
"""
|
||||||
state = hass.states.get(FAN_GROUP)
|
state = hass.states.get(FAN_GROUP)
|
||||||
# No entity has a valid state -> group state off
|
# No entity has a valid state -> group state unavailable
|
||||||
assert state.state == STATE_OFF
|
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
|
||||||
|
|
||||||
|
# Test group members exposed as attribute
|
||||||
|
hass.states.async_set(CEILING_FAN_ENTITY_ID, STATE_UNKNOWN, {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
state = hass.states.get(FAN_GROUP)
|
||||||
assert state.attributes[ATTR_ENTITY_ID] == [
|
assert state.attributes[ATTR_ENTITY_ID] == [
|
||||||
*FULL_FAN_ENTITY_IDS,
|
*FULL_FAN_ENTITY_IDS,
|
||||||
*LIMITED_FAN_ENTITY_IDS,
|
*LIMITED_FAN_ENTITY_IDS,
|
||||||
]
|
]
|
||||||
assert ATTR_ASSUMED_STATE not in state.attributes
|
|
||||||
assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0
|
# All group members unavailable -> unavailable
|
||||||
|
hass.states.async_set(CEILING_FAN_ENTITY_ID, STATE_UNAVAILABLE)
|
||||||
|
hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, STATE_UNAVAILABLE)
|
||||||
|
hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, STATE_UNAVAILABLE)
|
||||||
|
hass.states.async_set(PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_UNAVAILABLE)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
state = hass.states.get(FAN_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):
|
||||||
|
print("meh")
|
||||||
|
hass.states.async_set(CEILING_FAN_ENTITY_ID, state_1, {})
|
||||||
|
hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, state_2, {})
|
||||||
|
hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, state_3, {})
|
||||||
|
hass.states.async_set(
|
||||||
|
PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_UNKNOWN, {}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
state = hass.states.get(FAN_GROUP)
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
# The group state is off if all group members are off, unknown or unavailable.
|
# The group state is off if all group members are off, unknown or unavailable.
|
||||||
for state_1 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN):
|
for state_1 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||||
|
@ -141,32 +171,6 @@ async def test_state(hass, setup_comp):
|
||||||
state = hass.states.get(FAN_GROUP)
|
state = hass.states.get(FAN_GROUP)
|
||||||
assert state.state == STATE_OFF
|
assert state.state == STATE_OFF
|
||||||
|
|
||||||
for state_1 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN):
|
|
||||||
for state_2 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN):
|
|
||||||
for state_3 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN):
|
|
||||||
hass.states.async_set(CEILING_FAN_ENTITY_ID, state_1, {})
|
|
||||||
hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, state_2, {})
|
|
||||||
hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, state_3, {})
|
|
||||||
hass.states.async_set(
|
|
||||||
PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_UNAVAILABLE, {}
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
state = hass.states.get(FAN_GROUP)
|
|
||||||
assert state.state == STATE_OFF
|
|
||||||
|
|
||||||
for state_1 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN):
|
|
||||||
for state_2 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN):
|
|
||||||
for state_3 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN):
|
|
||||||
hass.states.async_set(CEILING_FAN_ENTITY_ID, state_1, {})
|
|
||||||
hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, state_2, {})
|
|
||||||
hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, state_3, {})
|
|
||||||
hass.states.async_set(
|
|
||||||
PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_UNKNOWN, {}
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
state = hass.states.get(FAN_GROUP)
|
|
||||||
assert state.state == STATE_OFF
|
|
||||||
|
|
||||||
# At least one member on -> group on
|
# At least one member on -> group on
|
||||||
for state_1 in (STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN):
|
for state_1 in (STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||||
for state_2 in (STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN):
|
for state_2 in (STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||||
|
@ -183,7 +187,7 @@ async def test_state(hass, setup_comp):
|
||||||
hass.states.async_remove(PERCENTAGE_LIMITED_FAN_ENTITY_ID)
|
hass.states.async_remove(PERCENTAGE_LIMITED_FAN_ENTITY_ID)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
state = hass.states.get(FAN_GROUP)
|
state = hass.states.get(FAN_GROUP)
|
||||||
assert state.state == STATE_OFF
|
assert state.state == STATE_UNKNOWN
|
||||||
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
|
||||||
|
|
||||||
|
@ -193,7 +197,7 @@ async def test_state(hass, setup_comp):
|
||||||
hass.states.async_remove(PERCENTAGE_FULL_FAN_ENTITY_ID)
|
hass.states.async_remove(PERCENTAGE_FULL_FAN_ENTITY_ID)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
state = hass.states.get(FAN_GROUP)
|
state = hass.states.get(FAN_GROUP)
|
||||||
assert state.state == STATE_OFF
|
assert state.state == STATE_UNAVAILABLE
|
||||||
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
|
||||||
|
|
||||||
|
@ -208,12 +212,9 @@ async def test_state(hass, setup_comp):
|
||||||
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(FAN_GROUP)
|
state = hass.states.get(FAN_GROUP)
|
||||||
assert state.state == STATE_OFF
|
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
|
||||||
*FULL_FAN_ENTITY_IDS,
|
|
||||||
*LIMITED_FAN_ENTITY_IDS,
|
|
||||||
]
|
|
||||||
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
|
||||||
hass.states.async_set(CEILING_FAN_ENTITY_ID, STATE_ON, {})
|
hass.states.async_set(CEILING_FAN_ENTITY_ID, STATE_ON, {})
|
||||||
|
@ -223,6 +224,10 @@ async def test_attributes(hass, setup_comp):
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
state = hass.states.get(FAN_GROUP)
|
state = hass.states.get(FAN_GROUP)
|
||||||
assert state.state == STATE_ON
|
assert state.state == STATE_ON
|
||||||
|
assert state.attributes[ATTR_ENTITY_ID] == [
|
||||||
|
*FULL_FAN_ENTITY_IDS,
|
||||||
|
*LIMITED_FAN_ENTITY_IDS,
|
||||||
|
]
|
||||||
|
|
||||||
# Add Entity that supports speed
|
# Add Entity that supports speed
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue