Set assumed state to group if at least one child has assumed state (#154163)

This commit is contained in:
Paul Bottein
2025-10-14 18:53:51 +02:00
committed by GitHub
parent 0eef44be91
commit cf477186aa
9 changed files with 227 additions and 7 deletions

View File

@@ -282,6 +282,7 @@ class CoverGroup(GroupEntity, CoverEntity):
self._attr_is_closed = True
self._attr_is_closing = False
self._attr_is_opening = False
self._update_assumed_state_from_members()
for entity_id in self._entity_ids:
if not (state := self.hass.states.get(entity_id)):
continue

View File

@@ -115,6 +115,17 @@ class GroupEntity(Entity):
def async_update_group_state(self) -> None:
"""Abstract method to update the entity."""
@callback
def _update_assumed_state_from_members(self) -> None:
"""Update assumed_state based on member entities."""
self._attr_assumed_state = False
for entity_id in self._entity_ids:
if (state := self.hass.states.get(entity_id)) is None:
continue
if state.attributes.get(ATTR_ASSUMED_STATE):
self._attr_assumed_state = True
return
@callback
def async_update_supported_features(
self,

View File

@@ -252,6 +252,7 @@ class FanGroup(GroupEntity, FanEntity):
@callback
def async_update_group_state(self) -> None:
"""Update state and attributes."""
self._update_assumed_state_from_members()
states = [
state

View File

@@ -205,6 +205,8 @@ class LightGroup(GroupEntity, LightEntity):
@callback
def async_update_group_state(self) -> None:
"""Query all members and determine the light group state."""
self._update_assumed_state_from_members()
states = [
state
for entity_id in self._entity_ids

View File

@@ -156,6 +156,8 @@ class SwitchGroup(GroupEntity, SwitchEntity):
@callback
def async_update_group_state(self) -> None:
"""Query all members and determine the switch group state."""
self._update_assumed_state_from_members()
states = [
state.state
for entity_id in self._entity_ids

View File

@@ -421,13 +421,6 @@ async def test_attributes(
assert ATTR_CURRENT_POSITION not in state.attributes
assert ATTR_CURRENT_TILT_POSITION not in state.attributes
# Group member has set assumed_state
hass.states.async_set(DEMO_TILT, CoverState.CLOSED, {ATTR_ASSUMED_STATE: True})
await hass.async_block_till_done()
state = hass.states.get(COVER_GROUP)
assert ATTR_ASSUMED_STATE not in state.attributes
# Test entity registry integration
entry = entity_registry.async_get(COVER_GROUP)
assert entry
@@ -859,6 +852,61 @@ async def test_is_opening_closing(hass: HomeAssistant) -> None:
assert hass.states.get(COVER_GROUP).state == CoverState.OPENING
@pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)])
@pytest.mark.usefixtures("setup_comp")
async def test_assumed_state(hass: HomeAssistant) -> None:
"""Test assumed_state attribute behavior."""
# No members with assumed_state -> group doesn't have assumed_state in attributes
hass.states.async_set(DEMO_COVER, CoverState.OPEN, {})
hass.states.async_set(DEMO_COVER_POS, CoverState.OPEN, {})
hass.states.async_set(DEMO_COVER_TILT, CoverState.CLOSED, {})
hass.states.async_set(DEMO_TILT, CoverState.CLOSED, {})
await hass.async_block_till_done()
state = hass.states.get(COVER_GROUP)
assert ATTR_ASSUMED_STATE not in state.attributes
# One member with assumed_state=True -> group has assumed_state=True
hass.states.async_set(DEMO_COVER, CoverState.OPEN, {ATTR_ASSUMED_STATE: True})
await hass.async_block_till_done()
state = hass.states.get(COVER_GROUP)
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
# Multiple members with assumed_state=True -> group has assumed_state=True
hass.states.async_set(
DEMO_COVER_TILT, CoverState.CLOSED, {ATTR_ASSUMED_STATE: True}
)
hass.states.async_set(DEMO_TILT, CoverState.CLOSED, {ATTR_ASSUMED_STATE: True})
await hass.async_block_till_done()
state = hass.states.get(COVER_GROUP)
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
# Unavailable member with assumed_state=True -> group has assumed_state=True
hass.states.async_set(DEMO_COVER, CoverState.OPEN, {})
hass.states.async_set(DEMO_COVER_TILT, CoverState.CLOSED, {})
hass.states.async_set(DEMO_TILT, STATE_UNAVAILABLE, {ATTR_ASSUMED_STATE: True})
await hass.async_block_till_done()
state = hass.states.get(COVER_GROUP)
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
# Unknown member with assumed_state=True -> group has assumed_state=True
hass.states.async_set(DEMO_TILT, STATE_UNKNOWN, {ATTR_ASSUMED_STATE: True})
await hass.async_block_till_done()
state = hass.states.get(COVER_GROUP)
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
# All members without assumed_state -> group doesn't have assumed_state in attributes
hass.states.async_set(DEMO_TILT, CoverState.CLOSED, {})
await hass.async_block_till_done()
state = hass.states.get(COVER_GROUP)
assert ATTR_ASSUMED_STATE not in state.attributes
async def test_nested_group(hass: HomeAssistant) -> None:
"""Test nested cover group."""
await async_setup_component(

View File

@@ -587,3 +587,47 @@ async def test_nested_group(hass: HomeAssistant) -> None:
assert hass.states.get(PERCENTAGE_FULL_FAN_ENTITY_ID).state == STATE_ON
assert hass.states.get("fan.bedroom_group").state == STATE_ON
assert hass.states.get("fan.nested_group").state == STATE_ON
async def test_assumed_state(hass: HomeAssistant) -> None:
"""Test assumed_state attribute behavior."""
await async_setup_component(
hass,
FAN_DOMAIN,
{
FAN_DOMAIN: [
{"platform": "demo"},
{
"platform": "group",
CONF_ENTITIES: [LIVING_ROOM_FAN_ENTITY_ID, CEILING_FAN_ENTITY_ID],
},
]
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
# No members with assumed_state -> group doesn't have assumed_state in attributes
hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, STATE_ON, {})
hass.states.async_set(CEILING_FAN_ENTITY_ID, STATE_OFF, {})
await hass.async_block_till_done()
state = hass.states.get(FAN_GROUP)
assert ATTR_ASSUMED_STATE not in state.attributes
# One member with assumed_state=True -> group has assumed_state=True
hass.states.async_set(
LIVING_ROOM_FAN_ENTITY_ID, STATE_ON, {ATTR_ASSUMED_STATE: True}
)
await hass.async_block_till_done()
state = hass.states.get(FAN_GROUP)
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
# All members without assumed_state -> group doesn't have assumed_state in attributes
hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, STATE_ON, {})
await hass.async_block_till_done()
state = hass.states.get(FAN_GROUP)
assert ATTR_ASSUMED_STATE not in state.attributes

View File

@@ -30,6 +30,7 @@ from homeassistant.components.light import (
ColorMode,
)
from homeassistant.const import (
ATTR_ASSUMED_STATE,
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
EVENT_CALL_SERVICE,
@@ -1647,3 +1648,72 @@ async def test_nested_group(hass: HomeAssistant) -> None:
assert hass.states.get("light.kitchen_lights").state == STATE_OFF
assert hass.states.get("light.bedroom_group").state == STATE_OFF
assert hass.states.get("light.nested_group").state == STATE_OFF
async def test_assumed_state(hass: HomeAssistant) -> None:
"""Test assumed_state attribute behavior."""
await async_setup_component(
hass,
LIGHT_DOMAIN,
{
LIGHT_DOMAIN: {
"platform": DOMAIN,
"entities": ["light.kitchen", "light.bedroom", "light.living_room"],
"name": "Light Group",
}
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
# No members with assumed_state -> group doesn't have assumed_state in attributes
hass.states.async_set("light.kitchen", STATE_ON, {})
hass.states.async_set("light.bedroom", STATE_ON, {})
hass.states.async_set("light.living_room", STATE_OFF, {})
await hass.async_block_till_done()
state = hass.states.get("light.light_group")
assert ATTR_ASSUMED_STATE not in state.attributes
# One member with assumed_state=True -> group has assumed_state=True
hass.states.async_set("light.kitchen", STATE_ON, {ATTR_ASSUMED_STATE: True})
await hass.async_block_till_done()
state = hass.states.get("light.light_group")
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
# Multiple members with assumed_state=True -> group has assumed_state=True
hass.states.async_set("light.bedroom", STATE_OFF, {ATTR_ASSUMED_STATE: True})
hass.states.async_set("light.living_room", STATE_OFF, {ATTR_ASSUMED_STATE: True})
await hass.async_block_till_done()
state = hass.states.get("light.light_group")
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
# Unavailable member with assumed_state=True -> group has assumed_state=True
hass.states.async_set("light.kitchen", STATE_ON, {})
hass.states.async_set("light.bedroom", STATE_OFF, {})
hass.states.async_set(
"light.living_room", STATE_UNAVAILABLE, {ATTR_ASSUMED_STATE: True}
)
await hass.async_block_till_done()
state = hass.states.get("light.light_group")
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
# Unknown member with assumed_state=True -> group has assumed_state=True
hass.states.async_set(
"light.living_room", STATE_UNKNOWN, {ATTR_ASSUMED_STATE: True}
)
await hass.async_block_till_done()
state = hass.states.get("light.light_group")
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
# All members without assumed_state -> group doesn't have assumed_state in attributes
hass.states.async_set("light.living_room", STATE_OFF, {})
await hass.async_block_till_done()
state = hass.states.get("light.light_group")
assert ATTR_ASSUMED_STATE not in state.attributes

View File

@@ -14,6 +14,7 @@ from homeassistant.components.switch import (
SERVICE_TURN_ON,
)
from homeassistant.const import (
ATTR_ASSUMED_STATE,
ATTR_ENTITY_ID,
STATE_OFF,
STATE_ON,
@@ -458,3 +459,43 @@ async def test_nested_group(hass: HomeAssistant) -> None:
assert hass.states.get("switch.decorative_lights").state == STATE_OFF
assert hass.states.get("switch.some_group").state == STATE_OFF
assert hass.states.get("switch.nested_group").state == STATE_OFF
async def test_assumed_state(hass: HomeAssistant) -> None:
"""Test assumed_state attribute behavior."""
await async_setup_component(
hass,
SWITCH_DOMAIN,
{
SWITCH_DOMAIN: {
"platform": DOMAIN,
"entities": ["switch.tv", "switch.soundbar"],
"name": "Media Group",
}
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
# No members with assumed_state -> group doesn't have assumed_state in attributes
hass.states.async_set("switch.tv", STATE_ON, {})
hass.states.async_set("switch.soundbar", STATE_OFF, {})
await hass.async_block_till_done()
state = hass.states.get("switch.media_group")
assert ATTR_ASSUMED_STATE not in state.attributes
# One member with assumed_state=True -> group has assumed_state=True
hass.states.async_set("switch.tv", STATE_ON, {ATTR_ASSUMED_STATE: True})
await hass.async_block_till_done()
state = hass.states.get("switch.media_group")
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
# All members without assumed_state -> group doesn't have assumed_state in attributes
hass.states.async_set("switch.tv", STATE_ON, {})
await hass.async_block_till_done()
state = hass.states.get("switch.media_group")
assert ATTR_ASSUMED_STATE not in state.attributes