Skip to content

Commit cf47718

Browse files
authored
Set assumed state to group if at least one child has assumed state (home-assistant#154163)
1 parent 0eef44b commit cf47718

File tree

9 files changed

+227
-7
lines changed

9 files changed

+227
-7
lines changed

homeassistant/components/group/cover.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ def async_update_group_state(self) -> None:
282282
self._attr_is_closed = True
283283
self._attr_is_closing = False
284284
self._attr_is_opening = False
285+
self._update_assumed_state_from_members()
285286
for entity_id in self._entity_ids:
286287
if not (state := self.hass.states.get(entity_id)):
287288
continue

homeassistant/components/group/entity.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,17 @@ def async_defer_or_update_ha_state(self) -> None:
115115
def async_update_group_state(self) -> None:
116116
"""Abstract method to update the entity."""
117117

118+
@callback
119+
def _update_assumed_state_from_members(self) -> None:
120+
"""Update assumed_state based on member entities."""
121+
self._attr_assumed_state = False
122+
for entity_id in self._entity_ids:
123+
if (state := self.hass.states.get(entity_id)) is None:
124+
continue
125+
if state.attributes.get(ATTR_ASSUMED_STATE):
126+
self._attr_assumed_state = True
127+
return
128+
118129
@callback
119130
def async_update_supported_features(
120131
self,

homeassistant/components/group/fan.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ def _set_attr_most_frequent(self, attr: str, flag: int, entity_attr: str) -> Non
252252
@callback
253253
def async_update_group_state(self) -> None:
254254
"""Update state and attributes."""
255+
self._update_assumed_state_from_members()
255256

256257
states = [
257258
state

homeassistant/components/group/light.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ async def async_turn_off(self, **kwargs: Any) -> None:
205205
@callback
206206
def async_update_group_state(self) -> None:
207207
"""Query all members and determine the light group state."""
208+
self._update_assumed_state_from_members()
209+
208210
states = [
209211
state
210212
for entity_id in self._entity_ids

homeassistant/components/group/switch.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ async def async_turn_off(self, **kwargs: Any) -> None:
156156
@callback
157157
def async_update_group_state(self) -> None:
158158
"""Query all members and determine the switch group state."""
159+
self._update_assumed_state_from_members()
160+
159161
states = [
160162
state.state
161163
for entity_id in self._entity_ids

tests/components/group/test_cover.py

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -421,13 +421,6 @@ async def test_attributes(
421421
assert ATTR_CURRENT_POSITION not in state.attributes
422422
assert ATTR_CURRENT_TILT_POSITION not in state.attributes
423423

424-
# Group member has set assumed_state
425-
hass.states.async_set(DEMO_TILT, CoverState.CLOSED, {ATTR_ASSUMED_STATE: True})
426-
await hass.async_block_till_done()
427-
428-
state = hass.states.get(COVER_GROUP)
429-
assert ATTR_ASSUMED_STATE not in state.attributes
430-
431424
# Test entity registry integration
432425
entry = entity_registry.async_get(COVER_GROUP)
433426
assert entry
@@ -859,6 +852,61 @@ async def test_is_opening_closing(hass: HomeAssistant) -> None:
859852
assert hass.states.get(COVER_GROUP).state == CoverState.OPENING
860853

861854

855+
@pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)])
856+
@pytest.mark.usefixtures("setup_comp")
857+
async def test_assumed_state(hass: HomeAssistant) -> None:
858+
"""Test assumed_state attribute behavior."""
859+
# No members with assumed_state -> group doesn't have assumed_state in attributes
860+
hass.states.async_set(DEMO_COVER, CoverState.OPEN, {})
861+
hass.states.async_set(DEMO_COVER_POS, CoverState.OPEN, {})
862+
hass.states.async_set(DEMO_COVER_TILT, CoverState.CLOSED, {})
863+
hass.states.async_set(DEMO_TILT, CoverState.CLOSED, {})
864+
await hass.async_block_till_done()
865+
866+
state = hass.states.get(COVER_GROUP)
867+
assert ATTR_ASSUMED_STATE not in state.attributes
868+
869+
# One member with assumed_state=True -> group has assumed_state=True
870+
hass.states.async_set(DEMO_COVER, CoverState.OPEN, {ATTR_ASSUMED_STATE: True})
871+
await hass.async_block_till_done()
872+
873+
state = hass.states.get(COVER_GROUP)
874+
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
875+
876+
# Multiple members with assumed_state=True -> group has assumed_state=True
877+
hass.states.async_set(
878+
DEMO_COVER_TILT, CoverState.CLOSED, {ATTR_ASSUMED_STATE: True}
879+
)
880+
hass.states.async_set(DEMO_TILT, CoverState.CLOSED, {ATTR_ASSUMED_STATE: True})
881+
await hass.async_block_till_done()
882+
883+
state = hass.states.get(COVER_GROUP)
884+
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
885+
886+
# Unavailable member with assumed_state=True -> group has assumed_state=True
887+
hass.states.async_set(DEMO_COVER, CoverState.OPEN, {})
888+
hass.states.async_set(DEMO_COVER_TILT, CoverState.CLOSED, {})
889+
hass.states.async_set(DEMO_TILT, STATE_UNAVAILABLE, {ATTR_ASSUMED_STATE: True})
890+
await hass.async_block_till_done()
891+
892+
state = hass.states.get(COVER_GROUP)
893+
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
894+
895+
# Unknown member with assumed_state=True -> group has assumed_state=True
896+
hass.states.async_set(DEMO_TILT, STATE_UNKNOWN, {ATTR_ASSUMED_STATE: True})
897+
await hass.async_block_till_done()
898+
899+
state = hass.states.get(COVER_GROUP)
900+
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
901+
902+
# All members without assumed_state -> group doesn't have assumed_state in attributes
903+
hass.states.async_set(DEMO_TILT, CoverState.CLOSED, {})
904+
await hass.async_block_till_done()
905+
906+
state = hass.states.get(COVER_GROUP)
907+
assert ATTR_ASSUMED_STATE not in state.attributes
908+
909+
862910
async def test_nested_group(hass: HomeAssistant) -> None:
863911
"""Test nested cover group."""
864912
await async_setup_component(

tests/components/group/test_fan.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,3 +587,47 @@ async def test_nested_group(hass: HomeAssistant) -> None:
587587
assert hass.states.get(PERCENTAGE_FULL_FAN_ENTITY_ID).state == STATE_ON
588588
assert hass.states.get("fan.bedroom_group").state == STATE_ON
589589
assert hass.states.get("fan.nested_group").state == STATE_ON
590+
591+
592+
async def test_assumed_state(hass: HomeAssistant) -> None:
593+
"""Test assumed_state attribute behavior."""
594+
await async_setup_component(
595+
hass,
596+
FAN_DOMAIN,
597+
{
598+
FAN_DOMAIN: [
599+
{"platform": "demo"},
600+
{
601+
"platform": "group",
602+
CONF_ENTITIES: [LIVING_ROOM_FAN_ENTITY_ID, CEILING_FAN_ENTITY_ID],
603+
},
604+
]
605+
},
606+
)
607+
await hass.async_block_till_done()
608+
await hass.async_start()
609+
await hass.async_block_till_done()
610+
611+
# No members with assumed_state -> group doesn't have assumed_state in attributes
612+
hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, STATE_ON, {})
613+
hass.states.async_set(CEILING_FAN_ENTITY_ID, STATE_OFF, {})
614+
await hass.async_block_till_done()
615+
616+
state = hass.states.get(FAN_GROUP)
617+
assert ATTR_ASSUMED_STATE not in state.attributes
618+
619+
# One member with assumed_state=True -> group has assumed_state=True
620+
hass.states.async_set(
621+
LIVING_ROOM_FAN_ENTITY_ID, STATE_ON, {ATTR_ASSUMED_STATE: True}
622+
)
623+
await hass.async_block_till_done()
624+
625+
state = hass.states.get(FAN_GROUP)
626+
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
627+
628+
# All members without assumed_state -> group doesn't have assumed_state in attributes
629+
hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, STATE_ON, {})
630+
await hass.async_block_till_done()
631+
632+
state = hass.states.get(FAN_GROUP)
633+
assert ATTR_ASSUMED_STATE not in state.attributes

tests/components/group/test_light.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
ColorMode,
3131
)
3232
from homeassistant.const import (
33+
ATTR_ASSUMED_STATE,
3334
ATTR_ENTITY_ID,
3435
ATTR_SUPPORTED_FEATURES,
3536
EVENT_CALL_SERVICE,
@@ -1647,3 +1648,72 @@ async def test_nested_group(hass: HomeAssistant) -> None:
16471648
assert hass.states.get("light.kitchen_lights").state == STATE_OFF
16481649
assert hass.states.get("light.bedroom_group").state == STATE_OFF
16491650
assert hass.states.get("light.nested_group").state == STATE_OFF
1651+
1652+
1653+
async def test_assumed_state(hass: HomeAssistant) -> None:
1654+
"""Test assumed_state attribute behavior."""
1655+
await async_setup_component(
1656+
hass,
1657+
LIGHT_DOMAIN,
1658+
{
1659+
LIGHT_DOMAIN: {
1660+
"platform": DOMAIN,
1661+
"entities": ["light.kitchen", "light.bedroom", "light.living_room"],
1662+
"name": "Light Group",
1663+
}
1664+
},
1665+
)
1666+
await hass.async_block_till_done()
1667+
await hass.async_start()
1668+
await hass.async_block_till_done()
1669+
1670+
# No members with assumed_state -> group doesn't have assumed_state in attributes
1671+
hass.states.async_set("light.kitchen", STATE_ON, {})
1672+
hass.states.async_set("light.bedroom", STATE_ON, {})
1673+
hass.states.async_set("light.living_room", STATE_OFF, {})
1674+
await hass.async_block_till_done()
1675+
1676+
state = hass.states.get("light.light_group")
1677+
assert ATTR_ASSUMED_STATE not in state.attributes
1678+
1679+
# One member with assumed_state=True -> group has assumed_state=True
1680+
hass.states.async_set("light.kitchen", STATE_ON, {ATTR_ASSUMED_STATE: True})
1681+
await hass.async_block_till_done()
1682+
1683+
state = hass.states.get("light.light_group")
1684+
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
1685+
1686+
# Multiple members with assumed_state=True -> group has assumed_state=True
1687+
hass.states.async_set("light.bedroom", STATE_OFF, {ATTR_ASSUMED_STATE: True})
1688+
hass.states.async_set("light.living_room", STATE_OFF, {ATTR_ASSUMED_STATE: True})
1689+
await hass.async_block_till_done()
1690+
1691+
state = hass.states.get("light.light_group")
1692+
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
1693+
1694+
# Unavailable member with assumed_state=True -> group has assumed_state=True
1695+
hass.states.async_set("light.kitchen", STATE_ON, {})
1696+
hass.states.async_set("light.bedroom", STATE_OFF, {})
1697+
hass.states.async_set(
1698+
"light.living_room", STATE_UNAVAILABLE, {ATTR_ASSUMED_STATE: True}
1699+
)
1700+
await hass.async_block_till_done()
1701+
1702+
state = hass.states.get("light.light_group")
1703+
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
1704+
1705+
# Unknown member with assumed_state=True -> group has assumed_state=True
1706+
hass.states.async_set(
1707+
"light.living_room", STATE_UNKNOWN, {ATTR_ASSUMED_STATE: True}
1708+
)
1709+
await hass.async_block_till_done()
1710+
1711+
state = hass.states.get("light.light_group")
1712+
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
1713+
1714+
# All members without assumed_state -> group doesn't have assumed_state in attributes
1715+
hass.states.async_set("light.living_room", STATE_OFF, {})
1716+
await hass.async_block_till_done()
1717+
1718+
state = hass.states.get("light.light_group")
1719+
assert ATTR_ASSUMED_STATE not in state.attributes

tests/components/group/test_switch.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
SERVICE_TURN_ON,
1515
)
1616
from homeassistant.const import (
17+
ATTR_ASSUMED_STATE,
1718
ATTR_ENTITY_ID,
1819
STATE_OFF,
1920
STATE_ON,
@@ -458,3 +459,43 @@ async def test_nested_group(hass: HomeAssistant) -> None:
458459
assert hass.states.get("switch.decorative_lights").state == STATE_OFF
459460
assert hass.states.get("switch.some_group").state == STATE_OFF
460461
assert hass.states.get("switch.nested_group").state == STATE_OFF
462+
463+
464+
async def test_assumed_state(hass: HomeAssistant) -> None:
465+
"""Test assumed_state attribute behavior."""
466+
await async_setup_component(
467+
hass,
468+
SWITCH_DOMAIN,
469+
{
470+
SWITCH_DOMAIN: {
471+
"platform": DOMAIN,
472+
"entities": ["switch.tv", "switch.soundbar"],
473+
"name": "Media Group",
474+
}
475+
},
476+
)
477+
await hass.async_block_till_done()
478+
await hass.async_start()
479+
await hass.async_block_till_done()
480+
481+
# No members with assumed_state -> group doesn't have assumed_state in attributes
482+
hass.states.async_set("switch.tv", STATE_ON, {})
483+
hass.states.async_set("switch.soundbar", STATE_OFF, {})
484+
await hass.async_block_till_done()
485+
486+
state = hass.states.get("switch.media_group")
487+
assert ATTR_ASSUMED_STATE not in state.attributes
488+
489+
# One member with assumed_state=True -> group has assumed_state=True
490+
hass.states.async_set("switch.tv", STATE_ON, {ATTR_ASSUMED_STATE: True})
491+
await hass.async_block_till_done()
492+
493+
state = hass.states.get("switch.media_group")
494+
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
495+
496+
# All members without assumed_state -> group doesn't have assumed_state in attributes
497+
hass.states.async_set("switch.tv", STATE_ON, {})
498+
await hass.async_block_till_done()
499+
500+
state = hass.states.get("switch.media_group")
501+
assert ATTR_ASSUMED_STATE not in state.attributes

0 commit comments

Comments
 (0)